Implementing a Custom Forward Pipe Operator for Function Chains in Swift
In essence, programs transform data. Input values are somehow manipulated and returned as output data. To keep the complexity of larger programs low, we break them up into smaller parts and abstractions, which are then composed later to form a bigger whole. Divide et impera.
As the name suggests, functional programming focuses on functions as abstractions on a low level. Most of the time, those are pure functions without side effects, meaning they don't change external state, making them safe to use and re-use.
Take the following two simple function definitions in Swift. They expect a single value of type Int
which is being incremented or squared, respectively:
func increment(x: Int) -> Int {
return x + 1
}
func square(x: Int) -> Int {
return x * x
}
To increment and afterwards square a value, you'd used the functions like this:
let value = 5
let transformed = square(increment(value))
The code works fine, but it's not perfect. We have to read the function applications inside-out. First, increment(value)
is evaluated, and the result of that expression is then passed to the square
function. Yet, from left to right, we write square
before increment
, contradicting the application order. Let's take a look at how F# handles this problem.
#The Forward Pipe Operator as Seen in F#
F# is a functional programming language that implements the so-called forward pipe operator, written as |>
. The operator passes the result on the left side to the function on the right side. Here's an example in F# that implements the two functions and combines them using the operator:
// Define the two functions
let increment x = x + 1
let square x = x * x
// Transform the value
let value = 5
let transformed = value |> increment |> square
As you can see, the data flow can be expressed clearly and concisely. The functions are written in the order they're applied, not backwards or inside-out. That makes it very easy to follow along the transformation of the value as it's passed through the function chain. It feels natural.
Alright, enough marveling about how great the forward pipe operator works in F# already. Let's implement it in Swift.
#Custom Operators in Swift
Swift allows you to define custom operators, which is a pretty cool thing. This is what the official language reference says about allowed characters:
Custom operators can begin with one of the ASCII characters
/
,=
,-
,+
,!
,*
,%
,<
,>
,&
,|
,^
,?
, or~
, or one of the Unicode characters defined in the grammar below (which include characters from the Mathematical Operators, Miscellaneous Symbols, and Dingbats Unicode blocks, among others). After the first character, combining Unicode characters are also allowed. Lexical Structure, Swift Language Reference
When you define an operator, you have to specify whether it's a prefix, an infix, or a postfix operator. Prefix and postfix operators both have a single operand; the operator is written before or after it, respectively. Infix operators have two operands and are denoted in between those.
#Implementing the Forward Pipe Operator in Swift
Since we want to apply the |>
operator (which doesn't exist in Swift natively) to two operands, we'll be defining an infix operator. We do this by writing the following operator declaration:
infix operator |> { associativity left precedence 80 }
The associativity left
keywords indicate that we want the operator to implicitly group values on its left side. This allows us to chain multiple calls to |>
without ambiguity. The following two lines are therefore equivalent:
let transformed1 = value |> increment |> square
let transformed2 = ((value |> increment) |> square)
Note that the order is important: Generally speaking, squaring an incremented value is not the same as incrementing a squared value.
We also specify a very low precedence level of 80 so that other operators will be applied first, preceding our passing the result through the function chain. For a complete reference table, please refer to the Binary Expressions section in the language reference.
After we've declared the operator, we have to provide a function implementing its functionality. This one is simple:
func |> <T, U>(value: T, function: (T -> U)) -> U {
return function(value)
}
The snippet above defines a function named |>
with two generic type parameters T
and U
and two arguments. T
is the type of the incoming value being passed to function
, which accepts one parameter of type T
and returns a value of type U
. The return type of the entire operator function is U
because that's the type of the value returned by function
.
That's actually all it takes to implement the forward pipe operator in Swift. We can now call it the same way and regain the naturally readable function ordering from left to right:
let value = 5
let transformed = value |> increment |> square
If the function chain is long, spanning the calls across multiple lines of code serves readability well:
let heavilyTransformed = value
|> increment
|> square
|> increment
|> square
The transformation process can now be read from top to bottom.
#Some Closing Notes
Being able to define custom operators in Swift opens up a tremendous amount of possibilities to extend the language. However, be reasonable and don't overdo it just because you can. But if an operator is easily understood and helps clarify your intent while at the same time simplifying your code, then go for it!