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 FPers use the words effects and effectful, but it can be hard to find a definition of what these terms mean.

Back to top

Effects are related to monads

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

As I wrote in my book, Functional Programming, Simplified, a slight simplification is to say that 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).

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("joe")

val x = for {
    a <- fInt()
    b <- fDouble()
    c <- fString()
} yield (a,b,c)
// x: Option[(Int, Double, String)] = Some((1,1.0,joe))

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 good enough for now.)

Back to top

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 — we’re talking about a main effect, i.e, the main purpose of each individual monad. An effect is what the monad does.

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
  • Future is a monad that models latency as an effect
  • Try is a monad that abstracts the effect of failures (manages exceptions as effects)

Similarly:

  • Reader is a monad that abstracts 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 abstracts the effect of state (composing a series of computations that maintain state)

So again, an effect is the thing a monad does. 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.

Back to top

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, the only logical thing to do is to return an instance of Option, i.e., a Some[Int] or a None. This is 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. 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).

Back to top

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.

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 abstracts the effect of failures (manages exceptions as effects)
  • Reader is a monad that abstracts 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 abstracts 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.

Back to top

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

Finally, in a shameless plug, if you’re interested in functional programming but parts of that conversation didn’t make sense, I encourage you to read my book, Functional Programming, Simplified:

Back to top

Comments

Permalink

The last time i had the feel to really understand a paradigma was when i read "Elegant Objects" by Yegor Bogayenko. Reading your book and this blog i have similar feelings now in the context of functional programming again.

Permalink

After reading this excellent post, I was compelled to buy a copy of pf-simplified. You have a real gift for explaining things, something we need much more of in the Scala world. I have been using Scala for over 10 years, and I am still struggling with grokking it.