One of the most exciting parts of writing this book was the exposure to the basic building blocks in the various languages of this book. For each new language, I introduced major new concepts. These are some of the programming constructs that you’re likely to see in other new languages you might discover. They are among my favorite discoveries.
As you saw in Erlang, Clojure, and Haskell,[30] the list comprehension is a compact structure that combines several ideas into a single powerful construct. A list comprehension has a filter, a map, and a Cartesian product.
We first encountered list comprehensions in Erlang. We started with a shopping cart of items such as Cart = [{pencil, 4, 0.25}, {pen, 1, 1.20}, {paper, 2, 0.20}]. To add tax to the list, we built a single list comprehension to solve the problem at once, like this:
8> WithTax = [{Product, Quantity, Price, Price * Quantity * 0.08} ||
|
|
8> {Product, Quantity, Price} <- Cart].
|
|
[{pencil,4,0.25,0.08},{pen,1,1.2,0.096},{paper,2,0.2,0.032}]
|
Several different language creators mention list comprehensions as one of their favorite features. I agree with this sentiment.
Perhaps the biggest intellectual growth for me was in the area of monads. With pure functional languages, we could not build programs with mutable state. Instead, we built monads that let us compose functions in a way that helped us structure problems as if they allowed mutable state. Haskell has do notation, supported by monads, to solve this problem.
We also found that monads allow us to simplify complex computation. Each of our monads supported a computational strategy. We used the Maybe monad to handle failure conditions, such as a List search that could potentially return Nothing. We used the List monad to compute a Cartesian product and unlock a combination.
One of the more common programming features we saw was pattern matching. We first encountered this programming construct in Prolog, but we also saw it in Scala, Erlang, Clojure, and Haskell. Each of these languages leaned on pattern matching to significantly simplify code. The problem domains included parsing, distributed message passing, destructuring, unification, XML processing, and more.
For a typical Erlang pattern match, recall the translate service:
| erlang/translate_service.erl | |
-module(translate_service).
|
|
-export([loop/0, translate/2]).
|
|
loop() ->
|
|
|
|
receive
|
|
{From, "casa"} ->
|
|
From ! "house",
|
|
loop();
|
|
|
|
|
|
{From, "blanca"} ->
|
|
From ! "white",
|
|
loop();
|
|
|
|
{From, _} ->
|
|
From ! "I don't understand.",
|
|
loop()
|
|
end.
|
|
|
|
translate(To, Word) ->
|
|
To ! {self(), Word},
|
|
receive
|
|
Translation -> Translation
|
|
end.
|
|
The loop function matched a process ID (From) followed by a word (casa or blanca) or a wildcard. The pattern match allows the programmer to quickly pick out the important pieces of the message without requiring any parsing from the programmer.
Prolog uses unification, a close cousin of pattern matching. You learned that Prolog would substitute possible values into a rule to force the left and right sides to match. Prolog would keep trying values until possibilities were exhausted. We looked at a simple Prolog program called concatenate as an example of unification:
| prolog/concat.pl | |
concatenate([], List, List).
|
|
concatenate([Head|Tail1], List, [Head|Tail2]) :-
|
|
concatenate(Tail1, List, Tail2).
|
|
We learned that unification makes this program so powerful because it could work in three ways: testing truth, matching the left side, or matching the right side.