Functions allow us to identify and extract reusable code. Let’s define a couple functions that make up the behavior above.
func incr(_ x: Int) -> Int {
return x + 1
}
func square(_ x: Int) -> Int {
return x * x
}
With these functions defined, we can pass them directly to map!
[1, 2, 3]
.map(incr)
.map(square)
// [4, 9, 16]
This refactor reads much better, but it’s less performant: we’re mapping over the array twice and creating an intermediate copy along the way! While we could use lazy to fuse these calls together, let’s take a more general approach: function composition!
[1, 2, 3].map(pipe(incr, square))
// [4, 9, 16]
The pipe function glues other functions together! It can take more than two arguments and even change the type along the way!
The function is the smallest building block of code. Function composition gives us the ability to fit these blocks together and build entire apps out of small, reusable, understandable units.
Examples
pipe
The most basic building block in Overture. It takes existing functions and smooshes them together. That is, given a function (A) -> B and a function (B) -> C, pipe will return a brand new (A) -> C function.
The with and update functions are useful for applying functions to values. They play nicely with the inout and mutable object worlds, wrapping otherwise imperative configuration statements in an expression.
class MyViewController: UIViewController {
let label = updateObject(UILabel()) {
$0.font = .systemFont(ofSize: 24)
$0.textColor = .red
}
}
And it restores the left-to-right readability we’re used to from the method world.
These functions make up the Swiss army knife of composition. They give us the power to take existing functions and methods that don’t compose (e.g, those that take zero or multiple arguments) and restore composition.
For example, let’s transform a string initializer that takes multiple arguments into something that can compose with pipe.
We use curry to transform multi-argument functions into functions that take a single input and return new functions to gather more inputs along the way.
And we use flip to flip the order of arguments. Multi-argument functions and methods typically take data first and configuration second, but we can generally apply configuration before we have data, and flip allows us to do just that.
Now we have a highly-reusable, composable building block that we can use to build pipelines.
let stringWithEncoding = flip(curry(String.init(data:encoding:)))
// (String.Encoding) -> (Data) -> String?
let utf8String = stringWithEncoding(.utf8)
// (Data) -> String?
Swift also exposes methods as static, unbound functions. These functions are already in curried form. All we need to do is flip them to make them more useful!
String.capitalized
// (String) -> (Locale?) -> String
let capitalized = flip(String.capitalized)
// (Locale?) -> (String) -> String
["hello, world", "and good night"]
.map(capitalized(Locale(identifier: "en")))
// ["Hello, World", "And Good Night"]
And zurry restores composition for functions and methods that take zero arguments.
get(\String.count)
// (String) -> Int
["hello, world", "and good night"]
.map(get(\.count))
// [12, 14]
We can even compose other functions into get by using the pipe function. Here we build a function that increments an integer, squares it, turns it into a string, and then gets the string’s character count:
pipe(incr, square, String.init, get(\.count))
// (Int) -> Int
let setUserName = prop(\User.name)
// ((String) -> String) -> (User) -> User
let capitalizeUserName = setUserName(capitalized(Locale(identifier: "en")))
// (User) -> User
let setUserAge = prop(\User.age)
let celebrateBirthday = setUserAge(incr)
// (User) -> User
with(User(name: "blob", age: 1), concat(
capitalizeUserName,
celebrateBirthday
))
// User(name: "Blob", age: 2)
over and set
The over and set functions produce (Root) -> Root transform functions that work on a Value in a structure given a key path (or setter function).
The over function takes a (Value) -> Value transform function to modify an existing value.
let celebrateBirthday = over(\User.age, incr)
// (User) -> User
The set function replaces an existing value with a brand new one.
with(user, set(\.name, "Blob"))
mprop, mver, and mut
The mprop, mver and mut functions are mutable variants of prop, over and set.
let guaranteeHeaders = mver(\URLRequest.allHTTPHeaderFields) { $0 = $0 ?? [:] }
let setHeader = { name, value in
concat(
guaranteeHeaders,
{ $0.allHTTPHeaderFields?[name] = value }
)
}
let request = update(
URLRequest(url: url),
mut(\.httpMethod, "POST"),
setHeader("Authorization", "Token " + token),
setHeader("Content-Type", "application/json; charset=utf-8")
)
zip and zip(with:)
This is a function that Swift ships with! Unfortunately, it’s limited to pairs of sequences. Overture defines zip to work with up to ten sequences at once, which makes combining several sets of related data a snap.
struct User {
let id: Int
let email: String
let name: String
}
zip(ids, emails, names).map(User.init)
// [
// User(id: 1, email: "blob@pointfree.co", name: "Blob"),
// User(id: 2, email: "blob.jr@pointfree.co", name: "Blob Junior"),
// User(id: 3, email: "blob.sr@pointfree.co", name: "Blob Senior")
// ]
Because of this, Overture provides a zip(with:) helper, which takes a tranform function up front and is curried, so it can be composed with other functions using pipe.
zip(with: User.init)(ids, emails, names)
Overture also extends the notion of zip to work with optionals! It’s an expressive way of combining multiple optionals together.
let optionalId: Int? = 1
let optionalEmail: String? = "blob@pointfree.co"
let optionalName: String? = "Blob"
zip(optionalId, optionalEmail, optionalName)
// Optional<(Int, String, String)>.some((1, "blob@pointfree.co", "Blob"))
And zip(with:) lets us transform these tuples into other values.
Using zip can be an expressive alternative to let-unwrapping!
let optionalUser = zip(with: User.init)(optionalId, optionalEmail, optionalName)
// vs.
let optionalUser: User?
if let id = optionalId, let email = optionalEmail, let name = optionalName {
optionalUser = User(id: id, email: email, name: name)
} else {
optionalUser = nil
}
FAQ
Should I be worried about polluting the global namespace with free functions?
Nope! Swift has several layers of scope to help you here.
You can limit exposing highly-specific functions beyond a single file by using fileprivate and private scope.
You can define functions as static members inside types.
You can qualify functions with the module’s name (e.g., Overture.pipe(f, g)). You can even autocomplete free functions from the module’s name, so discoverability doesn’t have to suffer!
Are free functions that common in Swift?
It may not seem like it, but free functions are everywhere in Swift, making Overture extremely useful! A few examples:
Initializers, like String.init.
Unbound methods, like String.uppercased.
Enum cases with associated values, like Optional.some.
Ad hoc closures we pass to map, filter, and other higher-order methods.
Top-level Standard Library functions like max, min, and zip.
Installation
You can add Overture to an Xcode project by adding it as a package dependency.
This library was created as an alternative to swift-prelude, which is an experimental functional programming library that uses infix operators. For example, pipe is none other than the arrow composition operator >>>, which means the following are equivalent:
We know that many code bases are not going to be comfortable introducing operators, so we wanted to reduce the barrier to entry for embracing function composition.
Interested in learning more?
These concepts (and more) are explored thoroughly in Point-Free, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.
The ideas in this episode were first explored in Episode #11:
License
All modules are released under the MIT license. See LICENSE for details.
🎼 Overture
A library for function composition.
Table of Contents
pipe
with
andupdate
concat
curry
,flip
, andzurry
get
prop
over
andset
mprop
,mver
, andmut
zip
Motivation
We work with functions all the time, but function composition is hiding in plain sight!
For instance, we work with functions when we use higher-order methods, like
map
on arrays:If we wanted to modify this simple closure to square our value after incrementing it, things begin to get messy.
Functions allow us to identify and extract reusable code. Let’s define a couple functions that make up the behavior above.
With these functions defined, we can pass them directly to
map
!This refactor reads much better, but it’s less performant: we’re mapping over the array twice and creating an intermediate copy along the way! While we could use
lazy
to fuse these calls together, let’s take a more general approach: function composition!The
pipe
function glues other functions together! It can take more than two arguments and even change the type along the way!Function composition lets us build new functions from smaller pieces, giving us the ability to extract and reuse logic in other contexts.
The function is the smallest building block of code. Function composition gives us the ability to fit these blocks together and build entire apps out of small, reusable, understandable units.
Examples
pipe
The most basic building block in Overture. It takes existing functions and smooshes them together. That is, given a function
(A) -> B
and a function(B) -> C
,pipe
will return a brand new(A) -> C
function.with
andupdate
The
with
andupdate
functions are useful for applying functions to values. They play nicely with theinout
and mutable object worlds, wrapping otherwise imperative configuration statements in an expression.And it restores the left-to-right readability we’re used to from the method world.
Using an
inout
parameter.concat
The
concat
function composes with single types. This includes composition of the following function signatures:(A) -> A
(inout A) -> Void
<A: AnyObject>(A) -> Void
With
concat
, we can build powerful configuration functions from small pieces.curry
,flip
, andzurry
These functions make up the Swiss army knife of composition. They give us the power to take existing functions and methods that don’t compose (e.g, those that take zero or multiple arguments) and restore composition.
For example, let’s transform a string initializer that takes multiple arguments into something that can compose with
pipe
.We use
curry
to transform multi-argument functions into functions that take a single input and return new functions to gather more inputs along the way.And we use
flip
to flip the order of arguments. Multi-argument functions and methods typically take data first and configuration second, but we can generally apply configuration before we have data, andflip
allows us to do just that.Now we have a highly-reusable, composable building block that we can use to build pipelines.
Swift also exposes methods as static, unbound functions. These functions are already in curried form. All we need to do is
flip
them to make them more useful!And
zurry
restores composition for functions and methods that take zero arguments.get
The
get
function produces getter functions from key paths.We can even compose other functions into
get
by using thepipe
function. Here we build a function that increments an integer, squares it, turns it into a string, and then gets the string’s character count:prop
The
prop
function produces setter functions from key paths.over
andset
The
over
andset
functions produce(Root) -> Root
transform functions that work on aValue
in a structure given a key path (or setter function).The
over
function takes a(Value) -> Value
transform function to modify an existing value.The
set
function replaces an existing value with a brand new one.mprop
,mver
, andmut
The
mprop
,mver
andmut
functions are mutable variants ofprop
,over
andset
.zip
andzip(with:)
This is a function that Swift ships with! Unfortunately, it’s limited to pairs of sequences. Overture defines
zip
to work with up to ten sequences at once, which makes combining several sets of related data a snap.It’s common to immediately
map
on zipped values.Because of this, Overture provides a
zip(with:)
helper, which takes a tranform function up front and is curried, so it can be composed with other functions usingpipe
.Overture also extends the notion of
zip
to work with optionals! It’s an expressive way of combining multiple optionals together.And
zip(with:)
lets us transform these tuples into other values.Using
zip
can be an expressive alternative tolet
-unwrapping!FAQ
Should I be worried about polluting the global namespace with free functions?
Nope! Swift has several layers of scope to help you here.
fileprivate
andprivate
scope.static
members inside types.Overture.pipe(f, g)
). You can even autocomplete free functions from the module’s name, so discoverability doesn’t have to suffer!Are free functions that common in Swift?
It may not seem like it, but free functions are everywhere in Swift, making Overture extremely useful! A few examples:
String.init
.String.uppercased
.Optional.some
.map
,filter
, and other higher-order methods.max
,min
, andzip
.Installation
You can add Overture to an Xcode project by adding it as a package dependency.
If you want to use Overture in a SwiftPM project, it’s as simple as adding it to a
dependencies
clause in yourPackage.swift
:🎶 Prelude
This library was created as an alternative to swift-prelude, which is an experimental functional programming library that uses infix operators. For example,
pipe
is none other than the arrow composition operator>>>
, which means the following are equivalent:We know that many code bases are not going to be comfortable introducing operators, so we wanted to reduce the barrier to entry for embracing function composition.
Interested in learning more?
These concepts (and more) are explored thoroughly in Point-Free, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.
The ideas in this episode were first explored in Episode #11:
License
All modules are released under the MIT license. See LICENSE for details.