Understanding Immutability, Closures, and Type Inference
A fundamental concept in functional programming is immutability: The values you create do not change. This is in
sharp contrast to object-oriented or imperative languages where mutation via variables is omnipresent. Instead of
mutating values, you create new mutated copies by replacing your ordinary imperative-style void functions that mutate
their parameters in-situ with functions that return a mutated copy of their parameters. You may wonder: Isn’t
creating new copies wasteful? Not quite, as it turns out the compiler is able to optimize much of the copy-then-mutate operations away by applying some clever tactics, which allows it to perform better in certain situations than imperative compilers.
Why do you need variables then? Strictly speaking, you don’t (unless you want to write imperative code). Consider the code segment below:
let printSumDoubled x y = printf “The result is = %d\n” ((x+y)*2)
let _ =
let a, b = 172, 201
printSumDoubled a b
let a, b = 215, 347
printSumDoubled a b
The first line defines a function that doubles the sum of its two arguments and prints the result. Note how no
type annotations were needed, the compiler figures out what parameters have what types by looking at how they are
used. Here, adding
x to
y gives it away that both
x and
y must be integers (by default for arithmetic operators, unless you annotate the parameters
otherwise). Another noteworthy piece is that you don’t have to parenthesize the formal parameters nor separate them
by commas. You can do that, but in that case your function no longer takes two arguments but instead one tuple
argument, and you need to change each invocation similarly to
printSumDoubled(a, b).
Don’t worry, the compiler tells you if you are passing too many arguments when calling functions, however,
passing fewer gives you a nifty way to create closures—functions that carry their own state bag:
let printSumWithFiveDoubled = printSumDoubled 5
Here
printSumWithFiveDoubled is a function that expects a single integer argument, adds five to it,
then doubles and prints the result. It is basically an instantiation of the
printSumDoubled function,
and this becomes more apparent in the following equivalent definition:
let printSumWithFiveDoubled y = printSumDoubled 5 y
The
let bindings in the above snippet simply combine individual assignments on a single line.
However, under the hood there is a lot more going on. In the first line, the snippet matches the value
(
172, 201) is matched against the pattern on the left (
a, b). Because that's a straightforward match, it results in binding those numbers to
a and
b,
respectively. Naturally, you could pattern match against arbitrary complex expressions and create numerous bindings
at once (or none at all—as in the top-level binding to
_ which simply means
throwing away the value of the computed expression). Functions that seemingly return no value
(those that are void in C#, for instance) have the
unit return type, which actually has a single
"no-value" represented as
(). The preceding example ignores this empty value.
The key piece is understanding that bindings are different from variables in the ordinary sense: they are nothing more than names for certain values, and reassigning them to new values simply shadows the previous ones. In effect, the above code is equivalent to:
let printSumDoubled x y = printf “The result is = %d\n” ((x+y)*2)
let _ =
let a, b = 172, 201
printSumDoubled a b
let newA, newB = 215, 347
printSumDoubled newA newB
Enhancing Your Types and Pattern Matching
Using
type augmentations is an effective way to manage the functionality provided around your types
(see
Listing 1).
Type definitions usually go hand-in-hand with some fundamental control abstraction mechanisms that operate on the
values that belong to those types: pattern matching (against the structure of a value—such as matching
a tree with a certain shape, or a list of a certain size and content), anonymous functions (or so-called lambda
expressions: functions created on the fly and without a name), and aggregate higher-order functions (that apply
functions on enumerable data). For instance, in
Listing 1, Seq.sumByFloat is a static aggregate
function on the Seq type (the type representing all enumerable types, and for which
seq is a standard type abbreviation, as used in the type definition for
Drawing) that takes an anonymous function (that extracts the perimeter of a shape), applies it on all shapes in the drawing, and sums up the results to get an indication of the amount of ink needed for the drawing.
Piping and Understanding Common Aggregate Functions
You may wonder about the definition of TotalInkNeeded using the pipe
( |>) operator. This operator is defined in the F# standard library as:
let (|>) x f = f x
Being an infix operator, the pipe sends (
pipes) its left operand to the right one, giving an elegant way to express a function call. This has at least two nice consequences: first, you can typically get by without having to parenthesize your arguments, second, you actually aid the compiler in type inference (as you are pushing type information from the left by supplying values of known types).
The sumByFloat method is a special case of folding, which is a more general functional pattern that takes a function and an initial accumulator and weaves them through a collection by applying the function with the initial accumulator to the first element, then with the result to the second, and so on, resulting in the final value of the accumulator. The F# standard library supports folding, mapping (applying a function to every element in a collection to construct a new collection with the resulting values), and iterating (visiting every element) on all generic enumerable types. These aggregate operations and many of the more special operations derived from them are the most heavily used tools in any functional programmer’s toolset.
And finally, test out your new types in an interactive session easily, using the code from
Listing 1. For example, the following code creates 20 new shapes (using a sequence comprehension and piping the result to the Drawing constructor) then calculates the total ink needed to draw them:
> let a = seq { for i in 1 .. 20
-> if i % 3 = 0 then
Circle (20, 20, i)
elif i % 3 = 1 then
Rectangle (20, 20, 20+i*2, 20+i)
else
Shape.Square (20, 20, i) } |> Drawing;;
val a : Drawing
> a.TotalInkNeeded;;
val it : float = 1123.840674
Conclusion
In this article, you saw some of the key functional programming concepts and how you can quickly get started using them in F#. You looked at some basic type definitions, simple, function, and structured values, infinite lazy sequences, and types with augmented members that used aggregate higher-order functions, lambda expressions, and the piping operator. You also created an enumeration of values representing graphical shapes using a sequence comprehension. In the next article of this series, you will learn about some of these features in detail and also about the more advanced functional constructs that can really drive your F# productivity curve.