Functional Basics: Expressions
The journey to master functional programming involves shifting one's approach of even the simplest problems. It is a different paradigm after all. But don't be intimidated, because this journey is best travelled by embracing small shifts in thinking. Unlearning small things and learning other (perhaps smaller) things. Previously we looked at what we could learn from the identity function, and now we'll explore the idea of shifting from the realm of statements to the domain of expressions. It is an essential transition, turning active mutations into passive evaluations, turning execution flows into compositions of values.
Expressions, unlike statements, are evaluations that yield a result and don't necessarily cause side effects - whereas statements inherently cause side effects.. Expressions aren't merely performed; they denote a computation that produces a value. Their purity makes them a vital part of the functional programming paradigm. Assigning an expression to a named binding can always be swapped out logically with the value of the evaluated expression. We call this referential transparency in functional programming, which makes it possible to perform beta reduction in lambda calculus.
Conditional Logic
A prime example lies in conditional logic. In imperative programming, we use if-else statements. However, in the functional world, these morph into conditional expressions. F#'s if...then...else
behaves as an expression rather than a statement, akin to the ternary (? :
) operator in languages like JavaScript or C#.
For instance, a function that evaluates the absolute value of a number can be written as let absolute x = if x < 0 then -x else x
. This if...then...else
is an expression that results in a value. In an imperative language, we would achieve the same with the code below. The most imporant thing to notice is, each path results in a return.
absolute(x) {
if (x < 0) {
return -x;
}
return x;
}
Collections
Now let's turn towards collection operations, which are absolutely necessary in any development project. Imperatively, we might initialize an empty list and fill it up in a loop. Functionally, we express this as a mapping operation. In F#, we can transform a list of integers into their squares using let squares = numbers |> List.map (fun x -> x * x)
, where numbers
is our original list. No mutations, no loops, just a pure expression. In C#, using expressions, we'd have numbers.Select(x => x * x)
.
While map
and Select
are quite common, virtually anything you could possibly do with collections can be done with expressions, utilizing things like reduce
, fold
, Aggregate
, permute
, etc. The main thing to remember for the student of functional programming is, anything you need to do with collections you can do with expressions - no matter how complex! The more complex, the better it will excercise your functional muscles.
Error Handling
Beyond these, numerous scenarios can be represented expressively. Error handling can transition from try-catch statements to expressive constructs like Option
or Result
in F#. For example, let tryDivide x y = if y = 0 then None else Some (x / y)
. This function wraps a potential error state (division by zero) in an Option
expression. In this scenario, we could utilize if...then...else
expressions, or better yet, pattern matching for control flow. Consider this:
match tryDivide 10 0 with
| Some quotient -> $"The result is {quotient}"
| None -> "You divided by zero"
While F# has built-in mechanisms for handling errors expressively with constructs like Option
and Result
, imperative languages are not without their expressive means. Languages such as JavaScript and C# have embraced certain aspects of functional programming and provide similar constructs for managing error states and nullable values.
Take JavaScript's Promise
construct for instance, which encapsulates a potentially asynchronous computation that may fail. Promises chain computations using .then()
and handle errors using .catch()
, allowing developers to express complex, asynchronous workflows without traditional error-prone callback hell. Similarly, in C#, the Nullable<T>
struct and the ??
operator can be used to expressively handle potential null values.
State Transformations
Another significant area to utilize esspressions is that of state transformations. Usually carried out imperatively by altering object properties, these can be expressed functionally by creating new states. Suppose we have a record let person = { Name = "Alice"; Age = 25 }
. Instead of mutating person
, we create a new state: let olderPerson = { person with Age = person.Age + 1 }
. Many languages have similar constructs such as the object spread operator in JS (let olderPerson = { ...person, age: person.age + 1}
), and the dictionary unpacking operator in Python. Be wary though that deeply nested objects may need the help of an expressive function for deep cloning objects.
Expressions reshape our perception of control flow and data manipulation, molding them into evaluations that yield values. Whether you're dealing with conditional logic, list operations, error handling, state transformation, or many other scenarios, the expressive world of functional programming offers a powerful and elegant perspective. Embrace it, and you'll find your code becoming more predictable, more testable, and easier to reason about. The transition may seem daunting, but remember, every step towards expression is a step towards mastering functional programming.