When you get started with functional programming (FP) a common question you’ll have is, “What is an effect in functional programming?” You’ll hear advanced FPers use the words effects and effectful, but it can be hard to find a definition of what these terms mean.
Effects are related to monads
A first step in the process of understanding effects is to say that they’re related to monads, so you have to know a wee bit about monads to understand effects.
As I wrote in my book, Functional Programming, Simplified, a slight simplification is to say that in Scala, a monad is any class that implements the map
and flatMap
methods. Because of the way Scala for-expressions work, implementing those two methods lets instances of that class be chained together in for-expressions (for/yield expressions).
(If you thought understanding monads was hard, I hope that helps to make them easier to understand.)
The benefit of monads
That leads to the benefit of monads in Scala: they let you sequence a series of operations together. For example, if every function returns an Option
, you can sequence the functions together in a for-expression:
def fInt(): Some[Int] = Some(1)
def fDouble(): Some[Double] = Some(1.0)
def fString(): Some[String] = Some("alvin")
val x = for {
a <- fInt()
b <- fDouble()
c <- fString()
} yield (a,b,c)
// x: Option[(Int, Double, String)] = Some((1,1.0,alvin))
Indeed, when people use the term monadic in Scala you can typically replace the word “monadic” with “in a for-expression.”
Summary: Step 1 is knowing that a monad in Scala is simply a class that implements
map
andflatMap
so it can be used in a for-expression. (That’s a slight over-simplification, but it’s good enough for now.)
Not a side effect, but the main effect
Now that we know that we’re talking about monads, the next important part is to understand the meaning of the word “effect.” A good way to describe this is to say that we’re not talking about a side effect — instead we’re talking about a main effect, i.e, the main purpose of each individual monad. An effect is what the monad handles.
This first became clear when I read the book, Functional and Reactive Domain Modeling, and came across these statements:
Option
models the effect of optionalityFuture
models latency as an effectTry
abstracts the effect of failures (manages exceptions as effects)
Those statements can also be written like this:
Option
is a monad that models the effect of optionality (of something being optional)Future
is a monad that models latency as an effectTry
is a monad that models the effect of failures (manages exceptions as effects)
Similarly:
Reader
is a monad that models the effect of composing operations that depend on some inputWriter
is a monad that models logging as an effectState
is a monad that models the effect of state (composing a series of computations that maintain state)
So again, an effect is the thing a monad handles. In terms of code, it’s how a monad implements its flatMap
method to achieve that effect.
Note: In Haskell, the equivalent of the
flatMap
method is known asbind
.
Effectful functions return F[A]
rather than [A]
In a YouTube video titled, Functional Programming with Effects, Rob Norris makes an interesting point: he says that an effectful function is a function that returns F[A]
rather than [A]
. For example, this function returns Option[Int]
rather than Int
:
def toInt(s: String): Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
}
In creating toInt
you could write a function that returns Int
, but the only ways to do that are:
- Return
0
if the conversion fails, or - Return
null
if the conversion fails
Regarding the first case, this is a bad idea because users will never know if the function received "0"
or something like "fred"
that won’t convert to a number. Regarding the second case, using null
is a bad practice in both OOP and FP, so that approach is just a bad idea.
Therefore, it occurs to you that a logical thing to do is to return an Option
from toInt
. Option
lets you handle the optional possible return values from toInt
:
Some[Int]
iftoInt
succeedsNone
if thetoInt
conversion fails
This is what it means when we say, “Option
is a monad that models the effect of optionality,” and it’s also what Mr. Norris means when he says that an effectful function returns F[A]
rather than [A]
. In the toInt
example:
F[A]
isOption[Int]
A
is the rawInt
type
Now, because toInt
is effectful — meaning that it returns an F[A]
, which is a monadic type — it can be used in for-expressions like this:
val x = for {
a <- toInt(string1)
b <- toInt(string2)
c <- toInt(string3)
} yield (a + b + c)
The result of this expression is that x
will be a Some[Int]
or a None
.
I don’t remember where I read it, but someone said that if you want to think about it philosophically, when a function returns an A
, that A
has already been fully evaluated; but if that function returns F[A]
instead, that result has not already been fully evaluated, the A
is still inside F[A]
waiting to be evaluated. So, rather than writing a function that returns a raw type, an effectful function returns a raw type inside a useful wrapper — where that wrapper is a monad that lets the result be used in a monadic style (i.e., in a Scala for-expression).
Summary
For some reason my brain has a hard time absorbing the words effect and effectful when people talk about things abstractly, so I decided to dig into this topic and then share my notes here. I hope you find them helpful as well.
As mentioned, rather than thinking of a side effect, an effect is the main effect of a monad that you’re using:
Option
is a monad that models the effect of optionalityFuture
is a monad that models latency as an effectTry
is a monad that models the effect of failures (manages exceptions as effects)Reader
is a monad that models the effect of composing operations that depend on some inputWriter
is a monad that models logging as an effectState
is a monad that models the effect of state (composing a series of computations that maintain state)
Furthermore, when a function is said to be effectful, it simply means that the function is returning a monad, i.e., some type F[A]
rather than the raw type A
.
Notes
In my programming life I need to move on to other topics, so I wrote this post quickly. It would be more effective if I showed you how to write flatMap
and map
functions in a monad, but I already did that in Functional Programming, Simplified, so I won’t repeat that here.
A few other notes:
- I oversimplified the definition of monad in that discussion. There are formal rules about monads that are important, and I discuss those in Functional Programming, Simplified. But a useful simplification is that any class that implements
map
is a functor, and any class that further implementsflatMap
(in addition tomap
) is a monad. - In the preceding discussion I used
Option
for my examples, but you can also use instances ofTry
orEither
, if you prefer. - I could have written
toInt
shorter (as shown below) but I wanted to clearly show the Option/Some/None types in the function body:
def toInt(s: String): Option[Int] = Try(Integer.parseInt(s.trim)).toOption