Published 2013-07-26.
Last modified 2019-07-12.
Time to read: 9 minutes.
This lecture discusses combinators, which define the data flow of a functional program, and monads, which are a central concept of functional programming. The Idioms and Writing for Clarity sections of this lecture address writing style.
The sample code for this lecture can be found in
courseNotes/
.
A combinator transforms input data into output data. Chaining combinators together defines the data flow of a functional program. The data pipeline computes results in a predictable manner because combinators perform transformations that are idempotent, which is a synonym for referentially transparent. In order to be idempotent, combinator implementations must not reference external state. Another way of saying this is that combinators must not have any bound variables.
so they are referentially transparent
Violating Referential Transparency
Here is an example program that shows how to violate this principle; this happens more often than you might think.
This example uses Akka to periodically run a task that modifies some state which is accessed from a method in another object.
We will explore Akka later in this course.
For now, we’ll just treat the scheduling code as a black box that somehow modifies externallyBoundVar
every 50 milliseconds.
object BoundAndGagged extends App {
case class Blarg(i: Int, s: String)
object OtherContext {
var externallyBoundVar = Blarg(0, "")
}
import OtherContext.externallyBoundVar
case class BadContainer(blarg: Blarg) {
/** This function is not a combinator because it accesses boundVar, which is external state.
BAD! */
def unpredictable(f: Blarg => Blarg): Blarg = f(blarg.copy(i=blarg.i + externallyBoundVar.i))
}
// start of black box
import akka.actor.ActorSystem
import concurrent.duration._
import concurrent.ExecutionContext.Implicits.global
val system = ActorSystem()
// Continuously modify externallyBoundVar on another thread
system.scheduler.schedule(0 milliseconds, 50 milliseconds) {
externallyBoundVar = if (System.currentTimeMillis % 2 == 0) externallyBoundVar else externallyBoundVar.copy(i=externallyBoundVar.i + 1)
}
// end of black box
val blarg = Blarg(1, "hello")
do {
val badContainer = BadContainer(blarg).unpredictable { _.copy(i=blarg.i*2) }
println(s"badContainer returned ${badContainer.i}")
if (badContainer.i>=3) {
system.terminate()
sys.exit(0)
}
} while (true)
}

The problem with this code is that
BadContainer.
’s output is not merely a function of the BadContainer
constructor parameters and the
function passed into unpredictable
– exernallyBoundVar
also affects the output of unpredictable
.
This means that the declared inputs to BadContainer
and unpredictable
don’t correspond to well-defined output.
In other words, each time you run unpredictable
for a given set of inputs you have no idea what the returned value might be.
For this reason, unpredictable
is not a combinator.
You can run this program as follows:
$ sbt "runMain BoundAndGagged" badContainer returned 3
Monads
Monads are a mathematical concept.
From a practical point of view, Scala monads are just container classes that implement three standard combinators: map
,
flatMap
and filter
.
Examples of monads are Option
, and each of the collection classes such as Set
, List
,
Vector
and Map
, as well as Future
,
which we will learn about in a series of lectures beginning with
MultiThreading.
Monads have predictable behavior because they implement the three required combinators,
plus a few others that are expected by convention:
collect
, flatten
and foreach
.
Many other combinators have come to known by convention for monads.
For example, Traversable
, which as you know is inherited by all Scala collections, defines dozens of combinators, including
exists
, fold
, groupBy
, head
and partition
.
Transforming Data with Combinators
The map
and flatMap
combinators both transform data stored in monads, and they share two useful properties.
- The result of invoking either of these combinators from a monad results in a new instance of the same type of monad.
- The type of data contained by the resulting monad can be different than the type of data contained in the original monad.
This cartoon has been floating around the interwebs since at least 2015, but I cannot determine the author:

map
Map is used to create a monad that may have a different type or value of contents from the original monad.
For example, let’s transform a collection of Int
into a new collection of Int
, but transform the contents so
they are divided by 2.
We’ll use a Vector
to hold the collection of Int
.
scala> val vector = Vector(0, 1, 2, 3) vector: scala.collection.immutable.Vector[Int] = Vector(0, 1, 2, 3)
scala> vector.map( _ / 2 ) res0: scala.collection.immutable.Vector[Int] = Vector(0, 0, 1, 1)
Notice that the result is a Vector
, which has the same type as the input, vector
.
The type of the data contained within the input Vector
(Int
) was the same for the output Vector
,
but this will not always be the case.
For example, we can convert each Int
in vector
into a String
by using map
like this.
scala> vector.map { i => "abcdefg".substring(0, 1 + i) } res1: scala.collection.immutable.Vector[String] = Vector(a, ab, abc, abcd)
curly braces must be used
Notice that I used {
curly braces }
around the lambda function passed to map
.
I could have used (
parentheses )
, however I used curly braces because the lambda function contained parentheses
and I felt that using curly braces delimited the lambda block better, and thereby made the code easier to read.
Identity Transform (Supplemental)
The identity transform returns a monad with the same data as the original monad.
scala> vector.map(identity) res2: scala.collection.immutable.Vector[Int] = Vector(0, 1, 2, 3)
Option and the fold Method
Option.map(someOperation).getOrElse(defaultValue)
is a very common idiom.
For Option
s, since an Option
is a monad that contains a single item, the fold
combinator can be
used instead.
For Option
s, you can think of fold
as being equivalent to.
def fold(defaultValue)(someOperation)
For example, given the following.
Some(42).map(_.toString).getOrElse("help!")
We can rewrite it using fold
this way.
Some(42).fold("help!")(_.toString)
flatten
flatten
removes the wrappings around a sequence of wrapped items and returns an new sequence with just the unwrapped values.
As an example, lets flatten a List
of List
s.
Remember that Nil
is a synonym for the empty list, which could also be written as List()
.
scala> List(List(1), Nil, List(2,3)).flatten res5: List[Int] = List(1, 2, 3)
Another example: given a sequence of Option
, flatten
will just return the elements with values wrapped in
instances of Some
; flatten
ignores sequence elements which are Nil
, None
or
null
.
scala> val vector2 = Vector(Some(1), None, Some(3), Some(4)) vector2: scala.collection.immutable.Vector[Option[Int]] = Vector(Some(1), None, Some(3), Some(4))
Invoking flatten will unwrap the Some
instances, discard the None
references and leave us with a
Vector[Int]
.
scala> vector2.flatten res6: Vector[Int] = Vector(1, 3, 4)
flatMap
flatMap
also flattens a sequence, but in addition flatMap
accepts a function value which is applied to each
sequence item, thereby transforming the sequence while flattening it.
In the following code example, the wrapped items with values (Some(1)
, Some(3)
and Some(4)
), are
unwrapped and passed to the lambda function.
This means that the None
items are not passed to the flatMap
’s lambda.
scala> vector2.flatMap { _.map(_*2) } res7: scala.collection.immutable.Vector[Int] = Vector(2, 6, 8)
We can write this same code using a for-comprehenshion.
The compiler desugars (removes the syntactic sugar) of the for-comprehension and generates code similar to the flatMap
/ map
code above.
scala> for { maybeItem <- vector2; item <- maybeItem } yield item * 2 res8: scala.collection.immutable.Vector[Int] = Vector(2, 6, 8)
Compare the result of flatMap
/ map
with that of map
/ map
.
scala> vector2.map { _.map(_*2) } res9: scala.collection.immutable.Vector[Option[Int]] = Vector(Some(2), None, Some(6), Some(8))
Supporting Combinators
The filter
in the next statement creates a new collection of the same type that only contains elements which pass the
predicate; only even Int
s are included in the resulting collection.
scala> vector.filter( _%2==0 ) res9: Vector[Int] = Vector(0, 2)
filterNot
includes the elements that fail the predicate test.
scala> vector.filterNot( _%2==0 ) res10: Vector[Int] = Vector(1, 3)
If you wanted both lists to be generated at once, that is, a Vector
of all items that pass the predicate and another
Vector
of all items that fail the predicate, you can use the partition
combinator.
scala> vector.partition( _%2==0 ) res11: (Vector[Int], Vector[Int]) = (Vector(0, 2),Vector(1, 3))
The above returned two lists, as you can see. These lists can be captured with one assignment, like this.
scala> val (pass, fail) = vector.partition( _%2==0 ) pass: Vector[Int] = Vector(0, 2) fail: Vector[Int] = Vector(1, 3)
scala> pass res12: Vector[Int] = Vector(2)
scala> fail res13: Vector[Int] = Vector(1, 3)
If you are concerned that readers of your code might be unclear as to the type of the assigned variables, you can explicitly declare their types.
scala> val (pass: Vector[Int], fail: Vector[Int]) = vector.partition( _%2==0 ) pass: Vector[Int] = Vector(0, 2) fail: Vector[Int] = Vector(1, 3)
You can prune out duplicate items from any collection that mixes in Seq
such as Vector
and List
by
using the distinct
method.
scala> Vector(1, 2, 2, 3).distinct res14: Vector[Int] = Vector(1, 2, 3)
exists
, find
and forall
are similar.
exists
returns true
if a predicate is true for at least one element in a collection.
find
returns an Option
containing the first element found in a collection that fulfills a predicate.
forall
only returns true if a predicate is true for all members of a vector.
scala> vector.exists(_%2==0) res15: Boolean = true
scala> vector.find(_%2==0) res16: Option[Int] = Some(0)
scala> vector.forall(_%2==0) res17: Boolean = false
Maps and groupBy
The map
method is distinct from the mutable.Map
and immutable.Map
types.
A Map
is a collection of name/value pairs, often provided to the constructor or map builder as Tuple2
instances.
The map
combinator method accepts an operation that will be applied to each element of a collection.
Yes, you can write code like this, but you should not.
scala> val map = Map(1 -> "a", 2 -> "b", 3 -> "c", 4 -> "d") map: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b, 3 -> c, 4 -> d)
scala> map.map { nv => (nv._1 * 3, nv._2 * 3) } res18: scala.collection.immutable.Map[Int,String] = Map(3 -> aaa, 6 -> bbb, 9 -> ccc, 12 -> ddd)
However, you would be more popular with the programmers who come after you if you renamed the map
variable to something more
descriptive.
For example, depending on what the variable is used for, it might be called initialValues
.
scala> val initialValues = Map(1 -> "a", 2 -> "b", 3 -> "c", 4 -> "d") map: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b, 3 -> c, 4 -> d)
scala> initialValues.map { nv => (nv._1 * 3, nv._2 * 3) } res18: scala.collection.immutable.Map[Int,String] = Map(3 -> aaa, 6 -> bbb, 9 -> ccc, 12 -> ddd)
You can use filter
to create a copy of a Map
with certain tuples filtered out.
The following two syntaxes are equivalent – they both return a new Map
containing only even keys.
scala> initialValues.filter( x => x._1 % 2 == 0 ) res19: scala.collection.immutable.Map[Int,String] = Map(2 -> b, 4 -> d)
scala> initialValues.filter( _._1 % 2 == 0 ) res20: scala.collection.immutable.Map[Int,String] = Map(2 -> b, 4 -> d)
groupBy
is a powerful Map
method.
Here we create a Map
of Map
s: one Map
that contains only even keys, and another Map
containing only odd keys.
The predicate passed into groupBy
determines the destination Map
that each name/value tuple in the original
Map
is assigned to.
scala> val group = initialValues.groupBy(_._1%2==0) group: scala.collection.immutable.Map[Boolean,scala.collection.immutable.Map[Int,String]] = Map(false -> Map(1 -> eh, 3 -> sea), true -> Map(2 -> bee, 4 -> d))
We can retrieve the submaps resulting from the groupBy
combinator easily.
scala> group(true) res21: scala.collection.immutable.Map[Int,String] = Map(2 -> b, 4 -> d)
scala> group(false) res22: scala.collection.immutable.Map[Int,String] = Map(1 -> eh, 3 -> sea)
Here is a better way of getting the tuples with even and odd keys from a Map
.
scala> val (even, odd) = iscala> nitialValues.partition(_._1%2==0) even: scala.collection.immutable.Map[Int,String] = Map(2 -> bee, 4 -> d) odd: scala.collection.immutable.Map[Int,String] = Map(1 -> eh, 3 -> sea)
Idioms
General Collection Idioms
There are many ways to express yourself in Scala, and some are less obvious than others.
Scala’s philosophy is to be reasonably brief, for some definition of ’reasonably’.
You should learn the following equivalent expressions, and favor the shorter expression when writing code.
The following will work on any Scala collection that inherits SeqLike
, including Option
, List
and Map
.
Given the following (the double
Function
is written using the placeholder syntax shorthand introduced in the
Functions are First Class lecture of the
Introduction to Scala course –
see the Lambda Review & Drill lecture if you need practice).
scala> val collection = List(1, 2, 3) collection: List[Int] = List(1, 2, 3)
scala> val double = (_:Int) * 2 double: Int => Int = <function1>
To remind you of how the double Function
works.
scala> double(21) res23: Int = 42
Then the following holds. Prefer the right-most of each pair of equivalent expressions (the underscores are examples of the higher-order shorthand syntax introduced in the Higher-Order Functions lecture of this course).
count
Awkward Expression | Preferred Equivalent Expression |
---|---|
collection.filter(_ > 0).size
|
collection.count(_ > 0)
|
Count is good for discovering how many items in a collection satisfy a predicate.
In this case, the predicate is the lambda _ > 0.
The awkward expression actually defines how count
works:
filter out all the items in the collection that do not satisfy the predicate, and obtain the size of the collection of the remaining items
which do satisfy the predicate.
scala> collection.filter(_ > 0).size res0: Int = 3
scala> collection.count(_ > 0) res1: Int = 3
nonEmpty
Awkward Expression | Preferred Equivalent Expression |
---|---|
!collection.isEmpty
|
collection.nonEmpty
|
While isEmpty
returns true
if the collection has no items in it, you don’t want to write a double negative
in order to test if the collection has any items.
The nonEmpty
property returns true
if the collection has at least one item in it.
scala> collection.isEmpty res14: Boolean = false
scala> !collection.isEmpty res15: Boolean = true
scala> collection.nonEmpty res16: Boolean = true
exists
Awkward Expression | Preferred Equivalent Expression |
---|---|
collection.filter(_ >= 2).length >= 1
|
collection.exists(_ >= 2)
|
collection.find(double(_) == 2).isDefined
| collection.exists(double(_) == 2)
|
If you want to discover if a collection contains at least one item whose value satisfies a predicate, use exists
instead of
testing the result of filter
or find
.
scala> collection.filter(_ >= 2).length >= 1 res4: Boolean = true
scala> collection.exists(_ == 2) res3: Boolean = true
scala> collection.find(double(_) == 2).isDefined res17: Boolean = true
scala> collection.exists(double(_) == 2) res18: Boolean = true
contains
Awkward Expression | Preferred Equivalent Expression |
---|---|
collection.find(_ == 2).isDefined
|
collection.contains(2)
|
collection.exists(_ == 2)
| collection.contains(2)
|
To discover if a collection contains a value, use contains
instead of testing the result of find
or calling
exists
and comparing to the desired value.
scala> collection.find(_ == 2).isDefined res19: Boolean = true
scala> collection.contains(2) res20: Boolean = true
scala> collection.exists(_ == 2) res21: Boolean = true
sum
Awkward Expression | Preferred Equivalent Expression |
---|---|
collection.fold(0)(_ + _)
|
collection.sum
|
collection.foldLeft(0)(_ + _)
| collection.sum
|
To add up all the values of a collection, use sum
instead of calling fold
or foldLeft
.
scala> collection.fold(0)(_ + _) res22: Int = 6
scala> collection.sum res23: Int = 6
scala> collection.foldLeft(0)(_ + _) res24: Int = 6
max and min
Awkward Expression | Preferred Equivalent Expression |
---|---|
collection.reduce(_ min _)
|
collection.min
|
collection.reduce((x, y) => math.min(x, y))
| collection.min
|
collection.reduceLeft(_ min _)
| collection.min
|
Call min
or max
to find the minimum or maximum value of a collection,
instead of calling reduce
or
reduceLeft
and passing in a lambda that invokes min
or max
.
scala> collection.reduce(_ min _) res25: Int = 1
scala> collection.min res26: Int = 1
scala> collection.reduce((x, y) => math.min(x, y)) res27: Int = 1
scala> collection.reduceLeft(_ min _) res28: Int = 1
scala> collection.max res29: Int = 3
Idioms Specific to Option
Given:
scala> val maybeHead = collection.headOption maybeHead: Option[Int] = Some(1)
... then the following holds. Again, prefer the right-most expression.
Awkward Expression | Preferred Equivalent Expression |
---|---|
maybeHead.map(_ + 1).getOrElse(0)
|
maybeHead.fold(0)(_ + 1)
|
Idioms Specific to Map and subclasses of MapOps
Given:
scala> val aMap = Map(’a’ -> 2) aMap: scala.collection.immutable.Map[Char,Int] = Map(a -> 2)
... then the following holds. Again, prefer the right-most expression.
Awkward Expression | Preferred Equivalent Expression |
---|---|
aMap.get(’a’).getOrElse(0)
|
aMap.getOrElse(’a’, 0)
|
Writing for Clarity

Functions vs. Methods
Higher order functions like map
and flatMap
accept lambda functions and regular named FunctionN
s.
If you supply a method where a FunctionN
is required, Scala automatically performs eta-expansion
(also referred to as method lifting) and creates a FunctionN
from the method.
Scala also treats FunctionN
s as if they were methods.
Anywhere you can write a method reference you can use a FunctionN
instead.
Conversely, anywhere you can write a FunctionN
reference you can write a method reference instead.
If you are writing a class or a trait that defines a method using a def
keyword, you could also define a
FunctionN
.
The FunctionN
will close over any variables or other methods that it references,
such as x
in this example.
We discussed closures in the Closures lecture of the
Introduction to Scala course.
object FuncMeth extends App { class Klass { val x = 3 def method1(y: Int) = s"x=$x and y=$y from method 1" val function1 = (y:Int) => s"x=$x and y=$y from function 1" } val klass = new Klass() println(klass.method1(4)) println(klass.function1(4)) }
You can run this by typing.
$ sbt "runMain FuncMeth" x=3 and y=4 from method 1 x=3 and y=4 from function 1
Good programmers write code that humans can understand.
The point of this section is: if you need some code which will mostly be used as a method, write it as a method.
If that code is mostly going to be used as a FunctionN
, write a FunctionN
.
The only benefit is so a reader can understand what your purpose is.
Since method lifting is done at compile time, your program will not run faster or slower if you write a method or a
FunctionN
.
Use Intermediate Variables and Declare Their Types
Here is some production code from the Silhouette open source project. It is hard to understand because the code offers no clue regarding the types used in the code. Instead, chains of combinators are written, one after the other. If one of the monads short-circuits there is no way to discover which monad terminated the flow, or why it short-circuited. This makes debugging difficult. This code is expensive to maintain as written.
This code is also hard to follow because types are declared as pure traits instead of concrete types, and the traits are bound to concrete types at runtime by Google Guice using dependency injection. There is no way to understand what this code does without spending considerable time analyzing it with an IDE.
This code uses the Future
monad.
We will discuss Future
s over the course of several lectures later in this course, starting with
MultiThreading.
Future
s are like any other monad, and the lesson taught here applies equally well to collection monads and any other type of
monad.
def authenticate(loginInfo: LoginInfo, password: String): Future[State] = { authInfoRepository.find[PasswordInfo](loginInfo).flatMap { case Some(passwordInfo) => passwordHasherRegistry.find(passwordInfo) match { case Some(hasher) if hasher.matches(passwordInfo, password) => if (passwordHasherRegistry.isDeprecated(hasher) || hasher.isDeprecated(passwordInfo).contains(true)) { authInfoRepository.update(loginInfo, passwordHasherRegistry.current.hash(password)).map { _ => Authenticated } } else { Future.successful(Authenticated) } case Some(hasher) => Future.successful(InvalidPassword(PasswordDoesNotMatch.format(id))) case None => Future.successful(UnsupportedHasher(HasherIsNotRegistered.format( id, passwordInfo.hasher, passwordHasherRegistry.all.map(_.id).mkString(", ") ))) } case None => Future.successful(NotFound(PasswordInfoNotFound.format(id, loginInfo))) } }
Here is the same code, rewritten to show intermediate variables and types. This makes the code more understandable, debuggable and therefore maintainable. This version does not run noticeably slower. I also added some conditional log output, which also does not noticeably impact runtime performance.
def authenticate(loginInfo: LoginInfo, password: String): Future[State] = { val maybeAuthInfoRepo: Future[Option[PasswordInfo]] = authInfoRepository.find[PasswordInfo](loginInfo) maybeAuthInfoRepo.flatMap { case Some(passwordInfo) => val maybeHasher: Option[PasswordHasher] = passwordHasherRegistry.find(passwordInfo) maybeHasher match { case Some(hasher) if hasher.matches(passwordInfo, password) => if (passwordHasherRegistry.isDeprecated(hasher) || hasher.isDeprecated(passwordInfo).contains(true)) { val hashedPwd: PasswordInfo = passwordHasherRegistry.current.hash(password) val updateResult: Future[PasswordInfo] = authInfoRepository.update(loginInfo, hashedPwd) updateResult.map { _ => logger.debug("Authenticated with deprecated password hasher!") Authenticated } } else { logger.debug("Authenticated with current password hasher!") Future.successful(Authenticated) } case Some(hasher) => Future.successful(InvalidPassword(PasswordDoesNotMatch.format(id))) case None => Future.successful(UnsupportedHasher(HasherIsNotRegistered.format( id, passwordInfo.hasher, passwordHasherRegistry.all.map(_.id).mkString(", ") ))) } case None => Future.successful(NotFound(PasswordInfoNotFound.format(id, loginInfo))) } }
Exercise - HTTP URL-Form Encoded Parameters
This is typical problem when working with form parameters in the controller of a Play Framework application.
The HTTP URL-Form encoded response body has type Map[String, List[String]]
, which means that each String
parameter name can have a list of String
values.
The Map
has no default value.
The challenge is to write a method that returns Boolean
value according to whether the map contains the key
"active"
.
If the key is present, and has the value List("true")
, return true
.
Return false
if the key is not present in the map, or if the value is List("false")
.
Here is some test data (emptyMap
, trueMap
and falseMap
).
scala> val emptyMap = Map.empty[String, List[String]] emptyMap: scala.collection.immutable.Map[String,List[String]] = Map()
scala> val trueMap = Map("active" -> List("true")) trueMap: scala.collection.immutable.Map[String,List[String]] = Map(active -> List(true))
scala> val falseMap = Map("active" -> List("false")) falseMap: scala.collection.immutable.Map[String,List[String]] = Map(active -> List(false))
Solution
Here is a clumsy solution:
scala> def clumsy(body: Map[String, List[String]]): Boolean = body.get("active") .getOrElse(Nil) .map(_.toBoolean) .headOption .getOrElse(false) clumsy: (body: Map[String,List[String]])Boolean
Let’s try it out.
scala> clumsy(emptyMap) res0: Boolean = false
scala> clumsy(trueMap) res1: Boolean = true
scala> clumsy(falseMap) res2: Boolean = false
Here is how I arrived at this:
scala> emptyMap.get("active") res3: Option[List[String]] = None
scala> falseMap.get("active") res4: Option[List[String]] = Some(List(false))
scala> trueMap.get("active") res5: Option[List[String]] = Some(List(true))
scala> emptyMap.get("active").getOrElse(Nil) res6: List[String] = List()
scala> emptyMap.get("active").getOrElse(Nil).map(_.toBoolean) res7: List[Boolean] = List()
scala> emptyMap.get("active").getOrElse(Nil).map(_.toBoolean).headOption res8: Option[Boolean] = None
scala> emptyMap.get("active").getOrElse(Nil).map(_.toBoolean).headOption.getOrElse(false) res9: Boolean = false
Here is a better solution.
scala> def better(body: Map[String, List[String]]): Boolean = body.getOrElse("active", List("false")) .head.toBoolean better: (body: Map[String,List[String]])Boolean
Let’s try it out:
scala> better(emptyMap) res10: Boolean = false
scala> better(trueMap) res11: Boolean = true
scala> better(falseMap) res12: Boolean = false
Here is how I arrived at this solution:
scala> emptyMap.getOrElse("active", List("false")) res13: List[String] = List(false)
scala> trueMap.getOrElse("active", List("false")) res14: List[String] = List(true)
scala> falseMap.getOrElse("active", List("false")) res15: List[String] = List(false)
scala> emptyMap.getOrElse("active", List("false")).head res16: String = false
scala> emptyMap.getOrElse("active", List("false")).head.toBoolean res17: Boolean = false
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.
I have only needed
identity
a few times. None of the examples were simple or straightforward. If I find a readily digestible example I will of course show it. Until then, this is reserved for cocktail hour at a Scala conference to show mastery of Scala trivia.Here is a useless example.
... is the same as:
flatten
andflatMap
are both discussed later in this lecture.