Scala: What do “effect” and “effectful” mean in functional programming?

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 functional programmers 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 (but don’t worry)

A first step in the process of understanding effects is to say that they’re related to monads, so you have to know just a wee bit about monads to understand effects.

But fear not, this is surprisingly simple. As I write 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 (i.e., for/yield expressions).

And 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 and flatMap 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 optionality
  • Future models latency as an effect
  • Try 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 effect
  • Try 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 input
  • Writer is a monad that models logging as an effect
  • State 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 as bind.

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] if toInt succeeds
  • None if the toInt 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] is Option[Int]
  • A is the raw Int 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 optionality
  • Future is a monad that models latency as an effect
  • Try 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 input
  • Writer is a monad that models logging as an effect
  • State 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 implements flatMap (in addition to map) is a monad.
  • In the preceding discussion I used Option for my examples, but you can also use instances of Try or Either, 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

EXTRA CREDIT: More information about effects from the Cats Effect website

I thought that since the Cats Effect website had the word “effect” in the name, they might have some additional good descriptions of what an effect is, and sure enough they do. Here are several links from their website, followed by the relevant quotes:

typelevel.org/cats-effect:

“(Cats Effect) provides a concrete tool, known as ‘the IO monad,’ for capturing and controlling actions, often referred to as “effects...”

typelevel.org/cats-effect/docs/concepts:

“An effect is a description of an action (or actions) that will be taken when evaluation happens.”

def foo(str: String): IO[String] = ???

“In the above snippet, printer and printAndRead are both effects: they describe an action — or in the case of printAndRead, actions plural — which will be taken when they are evaluated. foo is an example of a function which returns an effect. As a short-hand, such functions are often called ‘effectful’: foo is an effectful function.”

“Critically, a side-effect is not the same thing as an effect. An effect is a description of some action, where the action may perform side-effects when executed. The fact that effects are just descriptions of actions is what makes them much safer and more controllable. (Conversely, when a piece of code contains a side-effect, that action just happens.)”

“In Cats Effect, code containing side-effects should always be wrapped in one of the ‘special’ IO constructors.”

(Note: I modified those last two quotes a little bit.)

typelevel.org/cats-effect/docs/2.x/datatypes/io:

IO is a data type for encoding side effects as pure values, capable of expressing both synchronous and asynchronous computations.”

“A value of type IO[A] is a computation which, when evaluated, can perform effects before returning a value of type A.”

IO values are pure, immutable values, and thus preserve referential transparency, being usable in functional programming. An IO is a data structure that represents just a description of a side effectful computation.”

EXTRA CREDIT: Effect/Effectful quotes from the Philip Wadler paper

In addition to those quotes, the Philip Wadler paper titled, “Monads for functional programming” (available as a PDF, which you can search for) has these quotes about effects:

“The use of monads to structure functional programs is described. Monads provide a convenient framework for simulating effects found in other languages, such as global state, exception handling, output, or non-determinism.”

“Pure languages are easier to reason about”

“These notes describe one (recent advance), the use of monads to integrate impure effects into pure functional languages.”

“Pure functional languages have this advantage: all flow of data is made explicit. And this disadvantage: sometimes it is painfully explicit.”

“A program in a pure functional language is written as a set of equations. Explicit data flow ensures that the value of an expression depends only on its free variables. Hence substitution of equals for equals is always valid, making such programs especially easy to reason about.”

“This programming style regains some of the flexibility provided by various features of impure languages.”

Summary #2

Again, I hope these quotes from the Cats Effect website and the Wadler paper help to expand on my description of effect and effectful.