• Stars
    star
    1,139
  • Rank 40,911 (Top 0.9 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 6 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐ŸŽผ A library for function composition.

๐ŸŽผ Overture

CI

A library for function composition.

Table of Contents

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:

[1, 2, 3].map { $0 + 1 }
// [2, 3, 4]

If we wanted to modify this simple closure to square our value after incrementing it, things begin to get messy.

[1, 2, 3].map { ($0 + 1) * ($0 + 1) }
// [4, 9, 16]

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!

[1, 2, 3].map(pipe(incr, square, String.init))
// ["4", "9", "16"]

Function composition lets us build new functions from smaller pieces, giving us the ability to extract and reuse logic in other contexts.

let computeAndStringify = pipe(incr, square, String.init)

[1, 2, 3].map(computeAndStringify)
// ["4", "9", "16"]

computeAndStringify(42)
// "1849"

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.

let computeAndStringify = pipe(incr, square, String.init)

computeAndStringify(42)
// "1849"

[1, 2, 3].map(computeAndStringify)
// ["4", "9", "16"]

with and update

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.

with(42, pipe(incr, square, String.init))
// "1849"

Using an inout parameter.

update(&user, mut(\.name, "Blob"))

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.

let roundedStyle: (UIView) -> Void = {
  $0.clipsToBounds = true
  $0.layer.cornerRadius = 6
}

let baseButtonStyle: (UIButton) -> Void = {
  $0.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
  $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
}

let roundedButtonStyle = concat(
  baseButtonStyle,
  roundedStyle
)

let filledButtonStyle = concat(roundedButtonStyle) {
  $0.backgroundColor = .black
  $0.tintColor = .white
}

let button = with(UIButton(type: .system), filledButtonStyle)

curry, flip, and zurry

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.

String.init(data:encoding:)
// (Data, String.Encoding) -> String?

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.

curry(String.init(data:encoding:))
// (Data) -> (String.Encoding) -> String?

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.

flip(curry(String.init(data:encoding:)))
// (String.Encoding) -> (Data) -> String?

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.

String.uppercased
// (String) -> () -> String

flip(String.uppercased)
// () -> (String) -> String

let uppercased = zurry(flip(String.uppercased))
// (String) -> String

["hello, world", "and good night"]
  .map(uppercased)
// ["HELLO, WORLD", "AND GOOD NIGHT"]

get

The get function produces getter functions from key paths.

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

prop

The prop function produces setter functions from key paths.

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.

let ids = [1, 2, 3]
let emails = ["[email protected]", "[email protected]", "[email protected]"]
let names = ["Blob", "Blob Junior", "Blob Senior"]

zip(ids, emails, names)
// [
//   (1, "[email protected]", "Blob"),
//   (2, "[email protected]", "Blob Junior"),
//   (3, "[email protected]", "Blob Senior")
// ]

It's common to immediately map on zipped values.

struct User {
  let id: Int
  let email: String
  let name: String
}

zip(ids, emails, names).map(User.init)
// [
//   User(id: 1, email: "[email protected]", name: "Blob"),
//   User(id: 2, email: "[email protected]", name: "Blob Junior"),
//   User(id: 3, email: "[email protected]", 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? = "[email protected]"
let optionalName: String? = "Blob"

zip(optionalId, optionalEmail, optionalName)
// Optional<(Int, String, String)>.some((1, "[email protected]", "Blob"))

And zip(with:) lets us transform these tuples into other values.

zip(with: User.init)(optionalId, optionalEmail, optionalName)
// Optional<User>.some(User(id: 1, email: "[email protected]", name: "Blob"))

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.

https://github.com/pointfreeco/swift-overture

If you want to use Overture in a SwiftPM project, it's as simple as adding it to a dependencies clause in your Package.swift:

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-overture", from: "0.5.0")
]

๐ŸŽถ 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:

xs.map(incr >>> square)
xs.map(pipe(incr, square))

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:

video poster image

License

All modules are released under the MIT license. See LICENSE for details.

More Repositories

1

swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
Swift
12,150
star
2

swift-snapshot-testing

๐Ÿ“ธ Delightful Swift snapshot testing.
Swift
3,737
star
3

isowords

Open source game built in SwiftUI and the Composable Architecture.
Swift
2,667
star
4

swift-navigation

Bringing simple and powerful navigation tools to all Swift platforms, inspired by SwiftUI.
Swift
1,950
star
5

swift-dependencies

A dependency management library inspired by SwiftUI's "environment."
Swift
1,527
star
6

swift-tagged

๐Ÿท A wrapper type for safer, expressive code.
Swift
1,364
star
7

pointfreeco

๐ŸŽฌ The source for www.pointfree.co, a video series on functional programming and the Swift programming language.
Swift
1,098
star
8

episode-code-samples

๐Ÿ’พ Point-Free episode code.
Swift
950
star
9

swift-case-paths

๐Ÿงฐ Case paths extends the key path hierarchy to enum cases.
Swift
905
star
10

swift-parsing

A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.
Swift
847
star
11

swift-nonempty

๐ŸŽ A compile-time guarantee that a collection contains a value.
Swift
839
star
12

swift-custom-dump

A collection of tools for debugging, diffing, and testing your application's data structures.
Swift
795
star
13

swift-html

๐Ÿ—บ A Swift DSL for type-safe, extensible, and transformable HTML documents.
Swift
760
star
14

combine-schedulers

โฐ A few schedulers that make working with Combine more testable and more versatile.
Swift
701
star
15

swift-perception

Observable tools, backported.
Swift
536
star
16

swift-identified-collections

A library of data structures for working with collections of identifiable elements in an ergonomic, performant way.
Swift
529
star
17

swift-web

๐Ÿ•ธ A collection of Swift server-side frameworks for handling HTML, CSS, routing and middleware.
Swift
481
star
18

swift-prelude

๐ŸŽถ A collection of types and functions that enhance the Swift language.
Swift
469
star
19

swift-validated

๐Ÿ›‚ A result type that accumulates multiple errors.
Swift
392
star
20

swift-issue-reporting

Report issues in your application and library code as Xcode runtime warnings, breakpoints, assertions, and do so in a testable manner.
Swift
361
star
21

swift-url-routing

A bidirectional router with more type safety and less fuss.
Swift
347
star
22

swift-concurrency-extras

Useful, testable Swift concurrency.
Swift
315
star
23

swift-gen

๐ŸŽฑ Composable, transformable, controllable randomness.
Swift
266
star
24

swift-macro-testing

Magical testing tools for Swift macros.
Swift
263
star
25

swift-clocks

โฐ A few clocks that make working with Swift concurrency more testable and more versatile.
Swift
258
star
26

syncups

A rebuild of Appleโ€™s โ€œScrumdingerโ€ application using modern, best practices for SwiftUI development.
Swift
205
star
27

swift-enum-properties

๐Ÿค Struct and enum data access in harmony.
Swift
200
star
28

composable-core-location

A library that bridges the Composable Architecture and Core Location.
Swift
108
star
29

vapor-routing

A bidirectional Vapor router with more type safety and less fuss.
Swift
89
star
30

swift-html-vapor

๐Ÿ’ง Vapor plugin for type-safe, transformable HTML views.
Swift
84
star
31

swift-playground-templates

๐Ÿซ A collection of helpful Xcode playground templates.
Makefile
81
star
32

pointfreeco-server

Point-Free server code.
40
star
33

TrySyncUps

The starting project for our try! Swift 2024 Composable Architecture workshop.
Swift
38
star
34

composable-core-motion

A library that bridges the Composable Architecture and Core Motion.
Swift
29
star
35

swift-boundaries

๐Ÿฃ Functional core, imperative shell.
Swift
28
star
36

swift-quickcheck

๐Ÿ An implementation of QuickCheck in Swift.
Swift
25
star
37

swift-either

For those times you want A or B!
Swift
21
star
38

swift-algebras

Algebraic laws bundled into concrete data types.
20
star
39

swift-parser-printer

โ†”๏ธ Parsing and printing
Swift
15
star
40

swift-html-kitura

โ˜๏ธ Kitura plugin for type-safe, transformable HTML views.
Swift
14
star
41

swiftui-navigation

This package is now Swift Navigation:
Swift
13
star
42

homebrew-swift

Ruby
3
star
43

swift-bugs

3
star
44

Ccmark

Swift
2
star
45

.github

1
star