Back to Programming: Matching Parentheses
Learning Objectives
- You understand how a functional programming style program is built up from smaller functions
- You know how to approach real problems with functional programming
Writing software with Functional Programming
So far, we’ve only looked at individual functions, their signatures and implementations. However, these useful, but small, functions are not that useful on their own. Combining smaller functions into programs (larger functions) is where functional programming shines!
Consider a simple data processing problem. We’re given a List We’re given a String
consisting of lines, each containing a comma-separated list of numbers.
We want to calculate the sum of these numbers.
Let’s start designing our solution from the top level down.
The program would first have a function that splits the input into lines, a String -> List(String)
.
Then, for each lines, we’d apply a function that further splits that line into individual numbers, another String -> List(String)
.
Then, we’d have to parse the Strings
into numbers, of course, as responsible programmers, we’d also like to introduce some error handling: String -> Result(Int, String)
As for the error handling, we could use the try_fold
method implemented in the previous chapter, however, in this case we choose to just throw away the errors, perhaps printing them. List(Result(Int, String)) -> List(Int)
Finally, we can calculate the sum of the numbers to get our end result.
Now that we’ve laid out the program by figuring out the type signatures, let’s start implementing the functions. First, let’s give each function a name and a stump implementation:
fn lines(in: String) -> List(String) {
todo
}
fn csv(in: String) -> List(String) {
todo
}
fn parse_int(in: String) -> Result(Int, String) {
todo
}
fn filter_parsed(in: List(Result(Int, String))) -> List(Int) {
todo
}
fn sum(in: List(Int)) -> Int {
todo
}
Now that we’ve stumps for each function, we can start writing the implemetation. We could start from anywhere we want but let’s start implementing them in the order of “execution”, so that we can test parts of the program easily.
The Gleam standard library already provides an implementation for the lines
and csv
function in the
form of the string.split
function, so let’s use that.
Notice how we use list.map
from the standard library to apply csv
to each line returned by the lines
function.
It might seem a bit tedious to bind each return value to a new variable before passing it to the next function.
The other option would be to chain the function directly like this:
list.map(lines(in), csv)
However, the above code become difficult to read quite quickly.
Since chaining functions is such a common operation in functional programming, there are ways to make this easier to read.
Often programming languages implement something similar to Haskell’s $
-operator but Gleam does something different.
In Gleam, functions can be chained with the |>
-operator.
Using the pipe operator, the above program would look like this:
Let’s continue forward to the parse_int
function.
Again, the Gleam standard library is of use, providing the parse(string: String) -> Result(Int, Nil)
,
however its signature doesn’t quite match what we want.
We’d like the function to return a meaningful error message for failed parse-operations, so let’s do just that!
Notice, how we had to use an anonymous function in the function chain. Let’s give it a name and bring it out of the chain just to maintain readability:
Let’s now start the actual data processing. First of all, we’d like to get rid of the errors. It would also be nice to print them out to the users (NOTE: this introduces “side-effects” but we are fine with them).
Finally, let’s wrap it all together by implementing the sum function.
We can do it with the int.sum
function implemented by the Gleam standard library.
Now our program is ready! Something you might have noticed is that most of our functions are just renamed versions of their standard library implementations. This is something that might happen very often, once you start looking for a function you’ve implemented, you find that someone else has already done it way better than you. Writing small multi purpose functions is the essence of functional programming software.
Luckily, in our case, we didn’t go that far that we would’ve written the implementations ourselves, but let’s take one more step back and use the standard library implementations directly in our code.
After some cleanup, it should now be clear how functional programming programs are composed of chaining functions. Each program can be considered as a “big function”, that is itself made out of smaller functions.