// Java
void print123() {
System.out.println("1");
System.out.println("2");
System.out.println("3");
}
Easy IO
Conventional wisdom has it that IO is difficult, cumbersome, if not even impossible in pure functional programming.
Let’s see and compare the solutions for the simple case of a method (Java, Groovy) and function (Frege) that prints 1, 2, 3 to the console.
This looks like an overly simplistic example but it actually has some interesting lessons for us if we dare to question our familiar habits.
The code: Java vs Groovy vs Frege
Here is the code for comparison.
// Groovy
def print123() {
println "1"
println "2"
println "3"
}
The Groovy developer has a lot of choices. He can use the exact same syntax as in the Java version and it will be valid Groovy. Here is a rather compact version.
-- Frege
print123 = do
println "1"
println "2"
println "3"
Surprisingly, the Frege function is the shortest of all three, even slightly shorter than the Groovy version by 4 characters! Furthermore, it has the least amount of punctuation.
There is an obvious repetition in the examples and in both Groovy and Frege we could compress the three lines easily into a single one but we will leave that discussion for a future post.
But now comes the big surprise: Frege is not only the shortest, it is also the most explicit about the effect!
Who is more explicit?
Let’s see what each language announces in the type signature.
Java |
The method returns |
Groovy |
Groovy has pretty much the same characteristics as Java beside that it is more honest about Exceptions:
it does not even pretend to enforce them. The |
Frege |
The code does not mention any type at all but that doesn’t mean that there isn’t any. The type is inferred and when
we ask the REPL with |
The fundamental difference
The fundamental difference is that in an imperative language like Java and Groovy there is technically no
connection between the three println
statements. You can reorder them, you can interleave them with totally
unrelated computations, and you will not see that in the type signature.
The case is totally different in Frege. There are no statements, only expressions.
println "2"
and println "3"
are not just two lines that happen sit beneath each other.
They have a very strong connection. In fact, they are both expressions that happen to be an argument
to a function that binds the two together with a logic that the IO type specifies.
The return type of bind is the return type of its second argument. In our case IO ()
again.
-- pseudocode
bind (println "2") (println "3")
And, of course, the same is true for println "1"
.
-- pseudocode
bind (println "1") (bind (println "2") (println "3"))
Note
|
The bind does not appear as a function name in the IO type class -
only its operator symbol >>= , which is pronounced "bind".
So our pseudocode bind a b appears in real code as a >>= b .
|
The whole function body that looked so imperative boils down to a single expression and type inference becomes a piece of cake.
At this point, we have everything collapsed to a single expression until the do
keyword, which
is calling the bind function of that single expression. Since our expression is
of type IO
, the IO.bind
will be used.
We have seen before, for example in "Silent Notation", that functional programming sometimes means to read backwards. This is again true for the do-notation. This time it is not right-to-left but bottom-to-top to find the overall result type.
Note
|
A personal experience
It took me a while to understand how these mechanics work and in particular how on earth the do knows
which bind to use. There are many explanations around but that part was always missing for me -
probably because it was so obvious to everybody else.
|
As an aside, using bind like an infix operator
-- pseudocode a `bind` b `bind` c
reveals an interesting analogy. This is just like writing the imperative style of
a ; b ; c
This is why bind is also humorously known as the programmable semicolon. It provides the logic how two functions compose in a given context and - as we will see later - how the first function can bind its result to the argument of the second function.
Summary
-
IO code can be really easy to write even in a purely functional language like Frege.
-
Being purely functional does not mean that there is no IO. It means to be rigorously explicit about it.
-
Functional code can look like imperative code without sacrificing its functional nature.
-
Do-notation is your friend in the presence of side effects.