Chapter 2: Change Your Thinking

Functional programming requires a different way of thinking about the architecture of your software as compared to imperative programming. The first order of business is to stop thinking in terms of statement-based programming and start thinking in terms of expression-based programming. This means thinking in terms of systems of definitions and what can be derived from what, rather than sequential cause and effect. It will lead naturally to thinking in terms of immutable objects rather than the (mostly unconscious) mutable programming we’re all used to. Expression-based programming and immutable objects have direct consequences for a successful functional programmer and the ease with which we think about algorithms in a functional programming language.

Expression-based programming

What makes an imperative language imperative? Earlier I wrote:

“In computer science, imperative programming is a programming paradigm that describes computation in terms of statements that change a program state…imperative programs define sequences of commands for the computer to perform.”¹

In the previous quote is the key word “statement”. “In computer programming a statement is the smallest standalone element of an imperative programming language… [Statements do] not return results and are executed solely for their side effects.”² Obviously, in modern imperative languages, statements can also return values but they don’t have to.

Conversely, “an expression-oriented programming language is a programming language where every (or nearly every) construction is an expression and thus yields a value…All functional programming languages are expression-based.”³

A statement implicitly says “trust me and do this, I claim it’s the right thing to do,” whereas an expression says “here’s a way to derive a particular kind of result.” We can see the difference from our earlier examples.

A C# statement:

p.Offset(11, 12);

An F# expression:

let p2 = p.Offset 11 12

Void return in F#

In order to work with classes defined in the .NET framework, F# has to allow for statement-based programming and therefore allows a void return type. This opens a Pandora’s box of mutable functions; why would you call a function that returns nothing unless it has some side effect? So, for example, in C# we often write methods with a void return type:

public void PrintSomething(string s)
{
  Console.WriteLine(s);
}

Because F# is an impure language and needs to support void returns in order to work with the .NET framework and other imperative .NET languages, we can also write, in F# (FSI console):

let PrintSomething s =
  printfn "%s" s
  ();;
    
val PrintSomething : s:string -> unit

We are told this is a function that takes a string and returns a “unit,” F#’s word for “void.” The side effect is that something is emitted to the console window (its state changes). One should mostly avoid writing functions that return “unit,” as these imply that the function has side effects. There are perfectly valid reasons, such as persisting data to a database, but of course that is a side effect!

Eager expression evaluation

“In computer programming, eager evaluation or greedy evaluation is the evaluation strategy used by most traditional programming languages. In eager evaluation, an expression is evaluated as soon as it is bound to a variable. The alternative to eager evaluation is lazy evaluation, where expressions are only evaluated when evaluating a dependent expression. Imperative programming languages, where the order of execution is implicitly defined by the source code organization, almost always use eager evaluation.” ¹

Statements behave differently in statement-based and expression-based programming. In a statement-based language, at least within a small context such as a block, in the absence of other control logic, statements execute sequentially in the order in which they are defined. On the other hand, an impure expression-based language like F# allows statements to be embedded in expressions. In this case, the embedded statement will execute at the time the expression is evaluated, whenever that is. This is often difficult to wrap one’s head around at first, and results in some interesting behaviors. For example, let’s say you have a function that prints something and returns a literal (FSI console):

> let printAndReturn =
  printfn "Hi!"
  5;;
Hi!
    
val printAndReturn : int = 5
    
> printAndReturn;;
val it : int = 5

Note how Hi! is emitted immediately-F# performs “eager” expression evaluation by default. Furthermore, when we later call the function, because the expression has already been evaluated, the printfn statement embedded in it will not execute and Hi! is no longer emitted.

Conversely (FSI console):

> let Adding a b =
  printfn "Adding %d %d" a b
  a+b
;;
    
val Adding : a:int -> b:int -> int
    
> Adding 1 2;;
Adding 1 2
val it : int = 3
> Adding 3 4;;
Adding 3 4
val it : int = 7

Here, Adding cannot be evaluated until its parameters have been supplied. Therefore we get the console message Adding… each time we evaluate at the call site with Adding 1 2 or Adding 3 4.

Statements embedded in expressions can lead to a lot of confusion, for example when using printfn for debugging purposes. The expression will evaluate as soon as it can, not necessarily in the order that you would expect, especially coming from an imperative, statement-based, programming language.

Expression-based programming has other nuances. There are two fundamental evaluation strategies for expressions: eager and lazy. Let’s say you want to evaluate some expression based on whether a value is true or false:

> let Add a b =
    printfn "Computing a + b"
    a + b
    
let Subtract a b =
    printfn "Computing a - b"
    a - b
    
let Compute operation sum difference =
    match operation with
    | "Add" -> sum
    | "Subtract" -> difference
    | _ -> 0
    
Compute "Add" (Add 1 2) (Subtract 5 4)
;;
Computing a + b
Computing a - b
    
val Add : a:int -> b:int -> int
val Subtract : a:int -> b:int -> int
val Compute : operation:string -> sum:int -> difference:int -> int
val it : int = 3

Notice how the expressions (Add 1 2) and (Subtract 5 4) are both evaluated regardless of which expression result we want. This can potentially affect performance, so instead, we want to explicitly use lazy expression evaluation, which is discussed in the next section.