I Love Pure Functions, They Cannot Lie (Scala 3 Video)
One thing you’ll find in FP is that the signatures of pure functions tell you a lot about what those functions do. In fact, it turns out that the signatures of functions in FP applications are much more important than they are in OOP applications. As you’ll see in this lesson:
Because pure functions have no side effects, their outputs depend only on their inputs, and all FP values are immutable, pure function signatures tell you exactly what the function does.
OOP function signatures
When writing OOP applications I never gave much thought to method signatures. When working on development teams I always thought, “Meh, let me see the method source code so I can figure out what it really does.” I remember one time a junior developer wrote what should have been a simple Java “setter” method named setFoo
, and its source code looked something like this:
public void setFoo(int foo) {
this.foo = foo;
makeAMeal(foo);
foo++;
washTheDishes(foo);
takeOutTheTrash();
}
In reality I don’t remember everything that setter method did, but I clearly remember the foo++
part, and then saw that it the foo
and foo++
values in other method calls. A method that —according to its signature — appeared to be a simple setter method was in fact much, much more than that.
I hope you can see the problem here: there’s no way to know what’s really happening inside an impure function without looking at its source code.
The first moral of this story is that because OOP methods can have side effects, I grew to only trust methods from certain people.
The second moral is that this situation can’t happen with pure functions (at least not as blatanly as this).
Signatures of pure functions
The signatures of pure functions in Scala/FP have much more meaning than OOP functions because::
- They have no side effects
- Their output depends only on their inputs
- All values are immutable
To understand this, let’s play a simple game.
A game called, “What can this pure function possible do?”
As an example of this — and as a first thought exercise — look at this function signature and ask yourself, “If FOO
is a pure function, what can it possibly do?”:
def FOO(s: String): Int = ???
Ignore the name FOO
; I gave the function a meaningless name so you’d focus only on the rest of the type signature to figure out what this function can possibly do.
To solve this problem, let’s walk through some preliminary questions:
- Can this function read user input? It can’t have side effects, so, no.
- Can it write output to a screen? It can’t have side effects, so, no.
- Can it write (or read) information to (or from) a file, database, web service, or any other external data source? No, no, no, and no.
So what can it do?
If you said that there’s an excellent chance that this function does one of the following things, pat yourself on the back:
- Converts a
String
to anInt
- Determines the length of the input string
- Calculates a hashcode or checksum for the string
Because of the rules of pure functions, those are the only types of things this function can do. Output depends only on input.
A second game example
Here’s a second example that shows how the signatures of pure functions tell you a lot about what a function does. Given this simple class:
case class Person[name: String]
What can a pure function with this signature possibly do?:
def FOO(people: Seq[Person], n: Int): Person = ???
I’ll pause to let you think about it ...
By looking only at the function signature, you can guess that the function probably returns the nth element of the given List[Person]
.
That’s pretty cool. Because it’s a pure function you know that the Person
value that’s returned must be coming from the Seq[Person]
that was passed in.
Conversely, by removing the n
parameter from the function:
def FOO(people: Seq[Person]): Person = ???
Can you guess what this function can do?
(Pause to let you think ...)
My best guesses are:
- It’s a
head
function - It’s a
tail
function - It’s a Frankenstein’s Monster function that builds one
Person
from manyPerson
s
A third game example
Here’s a different variation of the “What can this pure function possibly do?” game. Imagine that you have the beginning of a function signature, where the input parameters are defined, but the return type is undefined:
def foo(s: String, i: Int) ...
Given only this information, can you answer the “What can this function possibly do?” question? That is, can you answer that question if you don’t know what the function’s return type is?
I’ll give you a little space to think about it ...
.
.
.
The answer is “no.” Even though foo
is a pure function, you can’t tell what it does until you see its return type. But ...
Even though you can’t tell exactly what it does, you can guess a little bit. For example, because output depends only on input, these return types are all allowed by the definition of a pure function:
def foo1(s: String, i: Int): Char = ???
def foo2(s: String, i: Int): String = ???
def foo3(s: String, i: Int): Int = ???
def foo4(s: String, i: Int): Seq[String] = ???
Even though you can’t tell what this function does without seeing its return type, I find this game fascinating. Where OOP method signatures had no meaning to me, I can make some really good guesses about what FP method signatures are trying to tell me — even when the function name is meaningless.
Trying to play the game with an impure method
Let’s look at one last example. What can this method possibly do?:
def foo(p: Person): Unit = ...
Because this method returns Unit
(nothing), it can also be written this way:
def foo(p: Person) { ... }
In either case, what do you think this method can do?
Because it doesn’t return anything, it must have a side effect of some sort. You can’t know what those side effects are, but you can guess that it may do any or all of these things:
- Write to STDOUT
- Write to a file
- Write to a database
- Write to a web service
- Update some other variable(s) with the data in
p
- Mutate the data in
p
- Ignore
p
and do something totally unexpected
As you can see, trying to understand what an impure method can possibly do is much more complicated than trying to understand what a pure function can possibly do. As a result of this, I came to understand this phrase:
Pure function signatures tell all.
Summary
As shown in this lesson, when a method has side effects there’s no telling what it does, but when a function is pure its signature lets you make very strong guesses at what it does — even when you can’t see the function name.
The features that make this possible are:
- The output of a pure function depends only on its inputs
- Pure functions have no side effects
- All values are immutable
What’s next
Now that I’ve written several small lessons about pure functions, the next two lessons will show how combining pure functions into applications feels both like (a) algebra and (b) Unix pipelines.
Update: All of my new videos are now on
LearnScala.dev