Pattern 15Chain of Operations

Intent

To chain a sequence of computations together—this allows us to work cleanly with immutable data without storing lots of temporary results.

Overview

Sending some bit of data through a set of operations is a useful technique. This is especially true when working with immutable data. Since we can’t mutate a data structure, we need to send an immutable one through a series of transformations if we want to make more than a single change.

Another reason we chain operations is because it leads to succinct code. For instance, the builder we saw in Pattern 4, Replacing Builder for Immutable Object, chains setting operations to keep our code lean, as the following snippet shows:

JavaExamples/src/main/java/com/mblinn/oo/javabean/PersonHarness.java
 
ImmutablePerson.Builder b = ImmutablePerson.newBuilder();
 
ImmutablePerson p = b.firstName(​"Peter"​).lastName(​"Jones"​).build();

Other times we chain method invocations to avoid creating noisy temporary values. In the code below we get a String value out of a List and uppercase it in one shot:

JavaExamples/src/main/java/com/mblinn/mbfpp/functional/coo/Examples.java
 
List​<​String​> names = ​new​ ​ArrayList​<​String​>();
 
names.add(​"Michael Bevilacqua Linn"​);
 
names.get(0).toUpperCase();

This style of programming is even more powerful in the functional world, where we have higher-order functions. For example, here we’ve got a snippet of Scala code that creates initials from a name:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
val​ name = ​"michael bevilacqua linn"
 
val​ initials = name.split(​" "​) map (_.toUpperCase) map (_.charAt(0)) mkString

It does so by calling split on the name, turning it into an array, then mapping functions over it that uppercase the strings and pick out the first character in each. Finally, we turn the array back into a string.

This is concise and declarative, so it reads nicely.

Sample Code: Function Call Chaining

Let’s take a look at a sample that involves several chained function calls. The objective is to write the code such that when we read it we can easily trace the flow of data from one step to the next.

We’ll take a vector of videos that represent a person’s video-viewing history, and we’ll calculate the total time spent watching cat videos. To do so, we’ll need to pick only the cat videos out of the vector, get their length, and finally add them together.

In Scala

For our Scala solution, we’ll represent videos as a case class with a title, video type, and length. The code to define this class and populate some test data follows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
case​ ​class​ Video(title: ​String​, video_type: ​String​, length: ​Int​)
 
 
val​ v1 = Video(​"Pianocat Plays Carnegie Hall"​, ​"cat"​, 300)
 
val​ v2 = Video(​"Paint Drying"​, ​"home-improvement"​, 600)
 
val​ v3 = Video(​"Fuzzy McMittens Live At The Apollo"​, ​"cat"​, 200)
 
 
val​ videos = Vector(v1, v2, v3)

To calculate the total time spent watching cat videos, we filter out videos where the video_type is equal to "cat", extract the length field from the remaining videos, and then sum those lengths. The code to do so follows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
def​ catTime(videos: Vector[Video]) =
 
videos.
 
filter((video) => video.video_type == ​"cat"​).
 
map((video) => video.length).
 
sum

Now we can apply catTime to our test data to get the total amount of time spent on cat videos:

 
scala>​ catTime(videos)
 
res0: Int = 500

This solution reads nicely from top to bottom, almost like prose. It does so without needing extra variables or any mutation, so it’s ideal in the functional world.

In Clojure

Let’s take a look at our cat-viewing problem in Clojure. Here, we’ll use maps for our videos. The following code snippet creates some test data:

ClojureExamples/src/mbfpp/functional/coo/examples.clj
 
(​def​ v1
 
{:title ​"Pianocat Plays Carnegie Hall"
 
:type :cat
 
:length 300})
 
 
(​def​ v2
 
{:title ​"Paint Drying"
 
:type :home-improvement
 
:length 600})
 
 
(​def​ v3
 
{:title ​"Fuzzy McMittens Live At The Apollo"
 
:type :cat
 
:length 200})
 
 
(​def​ videos [v1 v2 v3])

Let’s take a shot at writing cat-time in Clojure. As before, we’ll filter the vector of videos and extract their lengths. To sum up the sequence of lengths, we’ll use apply and the + function. The code for this solution follows:

ClojureExamples/src/mbfpp/functional/coo/examples.clj
 
(​defn​ cat-time [videos]
 
(​apply​ ​+
 
(​map​ :length
 
(​filter​ (​fn​ [video] (​=​ :cat (:type video))) videos))))

To understand this code, start with the filter function, move onto map, and then up to apply. For long sequences this can get tricky. One option would be to name the intermediate results using let to make things easier to understand.

Another option in this situation is to use Clojure’s -> and ->> macros. These macros can be used to thread a piece of data through a series of function calls.

The -> macro threads an expression through a series of forms, inserting it as the second item in each form. For instance, in the following snippet we use -> to thread an integer through two subtractions:

 
=> (-> 4 (- 2) (- 2))
 
0

The -> macro first threads 4 into the second position in (- 2), which subtracts 2 from 4 to get 2. Then, that result is threaded into the second slot in the later (- 2) to get a final result of 0.

If we use ->> we get a different result, as the following code snippet shows:

 
=> (->> 4 (- 2) (- 2))
 
4

Here, the ->> threads 4 into the last slot in the first (- 2), so 4 is subtracted from 2 to get a result of -2. That -2 is then threaded into the last slot of the second (-2), which subtracts a -2 from 2 to get a final result of 4.

Now that we’ve seen the threading operators, we can use ->> to make our original catTime read from top to bottom. We do so in the following snippet:

ClojureExamples/src/mbfpp/functional/coo/examples.clj
 
(​defn​ more-cat-time [videos]
 
(​->>​ videos
 
(​filter​ (​fn​ [video] (​=​ :cat (:type video))))
 
(​map​ :length)
 
(​apply​ ​+​)))

This works the same as our original:

 
=> (more-cat-time videos)
 
500

One limitation of the threading macros is that if we want to use them to chain function calls, the piece of data we’re passing through the chain of function calls must be consistently in the first or last position.

Sample Code: Chaining Using Sequence Comprehensions

A common use for Chain of Operations is that we need to perform multiple operations on values inside of some container type. This is especially common in statically typed languages like Scala.

For instance, we may have a series of Option values that we want to combine into a single value, returning None if any of them are None. There are several ways to do so, but the most concise relies on using a for comprehension to pick out the values and yield a result.

In Scala

We came across sequence comprehensions in Sample Code: Sequence Comprehensions, as a replacement for Iterator. Here we’ll take advantage of the fact that they can operate over more than one sequence at a time, which makes them useful for Chain of Operations.

Let’s take a look at a sequence comprehension that operates over two vectors, each with a single integer. We’ll use it to add the values in the vectors together. Our test vectors are defined in the following code:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
val​ vec1 = Vector(42)
 
val​ vec2 = Vector(8)

Here’s the for comprehension we use them with. We pick i1 out of the first vector and i2 out of the second, and we use yield to add them together:

 
scala>​ for { i1 <- vec1; i2 <- vec2 } yield(i1 + i2)
 
res0: scala.collection.immutable.Vector[Int] = Vector(50)

From there, it’s only a short hop to using for with Option. In the following code we define a couple of optional values:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
val​ o1 = Some(42)
 
val​ o2 = Some(8)

Now we can add them together as we did with the values out of our vectors.

 
scala>​ for { v1 <- o1; v2 <- o2 } yield(v1 + v2)
 
res1: Option[Int] = Some(50)

One advantage is that we don’t have to call get or pattern match to pull values out of Option. The power of this approach becomes more apparent when we add a None into the mix:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
val​ o3: ​Option​[​Int​] = None
 
scala>​ for { v1 <- o1; v3 <- o3 } yield(v1 + v3)
 
res2: Option[Int] = None

Now our for comprehension yields a None.

A Chain of Operations, each of which might yield a None, is common in Scala. Let’s take a look at an example that goes through a series of operations to retrieve a user’s list of favorite videos on a movie website.

To get the list of videos, we first need to look up a user by ID, then we need to look up the list of favorite videos by user. Finally, we need to look up the list of videos associated with that movie, such as cast interviews, trailers, and perhaps a full-length video of the movie itself.

We’ll start out by creating a couple of classes to represent a User and a Movie:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
case​ ​class​ User(name: ​String​, id: ​String​)
 
case​ ​class​ Movie(name: ​String​, id: ​String​)

Now we’ll define a set of methods to fetch a user, a favorite movie, and the list of videos for that movie. Each function returns None if it can’t find a response for its input. For this simple example we’ll do so using hardcoded values, but in real life this would likely involve a lookup from a database or service:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
def​ getUserById(id: ​String​) = id ​match​ {
 
case​ ​"1"​ => Some(User(​"Mike"​, ​"1"​))
 
case​ _ => None
 
}
 
 
def​ getFavoriteMovieForUser(user: User) = user ​match​ {
 
case​ User(_, ​"1"​) => Some(Movie(​"Gigli"​, ​"101"​))
 
case​ _ => None
 
}
 
 
def​ getVideosForMovie(movie: Movie) = movie ​match​ {
 
case​ Movie(_, ​"101"​) =>
 
Some(Vector(
 
Video(​"Interview With Cast"​, ​"interview"​, 480),
 
Video(​"Gigli"​, ​"feature"​, 7260)))
 
case​ _ => None
 
}

Now we can write a function to get a user’s favorite videos by chaining together calls to the functions we previously defined inside of a for statement:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/functional/coo/Examples.scala
 
def​ getFavoriteVideos(userId: ​String​) =
 
for​ {
 
user <- getUserById(userId)
 
favoriteMovie <- getFavoriteMovieForUser(user)
 
favoriteVideos <- getVideosForMovie(favoriteMovie)
 
} ​yield​ favoriteVideos

If we call getFavoriteVideos with a valid user ID, it’ll return the list of favorite videos.

 
scala>​ getFavoriteVideos("1")
 
res3: Option[scala.collection.immutable.Vector[...] =
 
Some(Vector(Video(Interview With Cast,interview,480),
 
Video(Gigli,feature,7260)))

If we call it with a user who doesn’t exist, the whole chain will return None instead:

 
scala>​ getFavoriteVideos("42")
 
res4: Option[scala.collection.immutable.Vector[...]] = None

In Clojure

Since Clojure isn’t statically typed, it doesn’t have anything like Scala’s Option as a core part of the language.

However, Clojure’s sequence comprehensions do work much like Scala’s for other container types. For instance, we can use for to pick out their contents and add them together, as we did in our Scala example. In the following code snippet, we do just that:

ClojureExamples/src/mbfpp/functional/coo/examples.clj
 
(​def​ v1 [42])
 
(​def​ v2 [8])
 
=> (for [i1 v1 i2 v2] (+ i1 i2))
 
(50)

If one of our vectors is the empty vector, then for will result in an empty sequence. The following code demonstrates:

ClojureExamples/src/mbfpp/functional/coo/examples.clj
 
(​def​ v3 [])
 
=> (for [i1 v1 i3 v3] (+ i1 i3))
 
()

Even though Clojure’s sequence comprehension works much the same as Scala’s, the lack of static typing and the Option type means that the sort of chaining we saw in Scala isn’t idiomatic. Instead we generally rely on chaining together functions with explicit null checks.

The flexibility of Lisp makes it possible to add on even something as fundamental as a static type checker into the language as a library. Just such a library is currently under development in the core.typed library,[6] which provides optional static typing.

As this library gains maturity, the type of chaining we saw in the Scala examples may become more and more common.

Discussion

The examples we saw in Sample Code: Chaining Using Sequence Comprehensions, are examples of the sequence or list monad. While we didn’t define exactly what a monad is, we did show a basic example of the sort of problems that they can solve. They make it natural to chain together operations on a container type while operating on the data inside of the container.

In the programming world, monads are most commonly known as a way to get IO and other nonpure features into a purely functional language. From the examples we saw above, it may not be immediately apparent what monads have to do with IO in a purely functional language.

Since neither Scala nor Clojure make use of monads in this way, we won’t go into it in detail here. The general reason, however, is that the monadic container type can carry along some extra information through the call chain. For instance, a monad to do IO would gather up all of the IO done through the Chain of Operations and then hand it off to a runtime when done. The runtime would then be responsible for performing the IO.

This style of programming was pioneered by Haskell. The curious reader can find an excellent introduction to it in Learn You a Haskell for Great Good!: A Beginner’s Guide [Lip11].

For Further Reading

Learn You a Haskell for Great Good!: A Beginner’s Guide [Lip11]

Related Patterns

Pattern 4, Replacing Builder for Immutable Object

Pattern 5, Replacing Iterator

Pattern 14, Filter-Map-Reduce

Pattern 16, Function Builder

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.10.1