Skip to content

davdroman/swift-builders

Folders and files

NameName
Last commit message
Last commit date

Latest commit

355a050 · Jul 10, 2024

History

55 Commits
Jul 10, 2024
Jul 10, 2024
Oct 25, 2022
Jul 10, 2024
Jul 10, 2024
Jul 10, 2024
Oct 25, 2022
Jun 11, 2022
Jun 9, 2022
Jun 9, 2022
Jul 10, 2024
Jun 14, 2022

Repository files navigation

Swift Builders

CI

A collection of useful result builders for Swift and Foundation value types.

Motivation

Arrays, dictionaries, and other collection-based types in Swift are relatively simple to construct and mutate.

However, things get tricky when the contained elements depend on certain conditions or awkward logic. A prime example of this is constructing a payload to send to an analytics service. The resulting code might look like this:

func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    var event: [String: String] = [:]
    event["success"] = didSucceed ? "true" : "false"
    if purchaseAmount > 0 {
        event["amount"] = purchaseAmount.formatted(.number.precision(.fractionLength(2)))
    } else {
        event["isFree"] = "true"
    }
    if let userId = userId {
        event["userId"] = userId
    } else {
        event["isGuest"] = "true"
    }
    return event
}

It's not bad, but it's definitely not as Swifty as one would expect.

We're sprinkling imperative code on what should just be a description of our payload. Not only does this make it harder to reason about the code at a glance, but it also leaves too much leeway for unintended mutations.

Thankfully, there's a better way...

Getting started

Swift Builders enables result builder syntax for most Collection types in Swift and Foundation.

For example, by leveraging Dictionary.build, our use case above becomes:

import Builders

func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    return [String: String].build {
        ["success": didSucceed ? "true" : "false"]
        if purchaseAmount > 0 {
            ["amount": purchaseAmount.formatted(.number.precision(.fractionLength(2)))]
        } else {
            ["isFree": "true"]
        }
        if let userId = userId {
            ["userId": userId]
        } else {
            ["isGuest": "true"]
        }
    }
}

We can even annotate our function with the @DictionaryBuilder attribute to make the function body behave like the builder body itself (think @ViewBuilder):

import Builders

@DictionaryBuilder<String, String>
func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    ["success": didSucceed ? "true" : "false"]
    if purchaseAmount > 0 {
        ["amount": purchaseAmount.formatted(.number.precision(.fractionLength(2)))]
    } else {
        ["isFree": "true"]
    }
    if let userId = userId {
        ["userId": userId]
    } else {
        ["isGuest": "true"]
    }
}

This is only a small demonstration of the power of result builders applied to Swift's native types.

The library offers a variety of builders out of the box:

  • ArrayBuilder
  • ArraySliceBuilder
  • ContiguousArrayBuilder
  • DataBuilder
  • DictionaryBuilder
  • SetBuilder
  • SliceBuilder
  • StringBuilder
  • StringUTF8ViewBuilder
  • StringUnicodeScalarViewBuilder
  • SubstringBuilder
  • SubstringUTF8ViewBuilder
  • SubstringUnicodeScalarViewBuilder

Benchmarks

MacBook Pro (14-inch, 2021)
Apple M1 Pro (10 cores, 8 performance and 2 efficiency)
32 GB Memory

$ swift run -c release Benchmarks

name                              time        std        iterations
-------------------------------------------------------------------
Array<Any>.build                  1833.000 ns ±   7.60 %     757726
Array<Int>.build                   542.000 ns ±  15.49 %    1000000
Array<Int?>.build                  709.000 ns ±   9.51 %    1000000
ArraySlice<Any>.build             2750.000 ns ±   5.28 %     511759
ArraySlice<Int>.build              875.000 ns ±   8.40 %    1000000
ArraySlice<Int?>.build            1167.000 ns ±  13.55 %    1000000
ContiguousArray<Any>.build        1917.000 ns ±  12.37 %     729365
ContiguousArray<Int>.build         542.000 ns ±  23.24 %    1000000
ContiguousArray<Int?>.build        750.000 ns ±  13.97 %    1000000
Data.build                         875.000 ns ±  13.55 %    1000000
Dictionary<String, Any>.build     4209.000 ns ±   6.26 %     328025
Dictionary<String, Double>.build  2459.000 ns ±  11.92 %     562007
Dictionary<String, Double?>.build 2583.000 ns ±   5.51 %     526636
Set<Any>.build                    6333.000 ns ±  10.30 %     228224
Set<Int>.build                     750.000 ns ±  11.22 %    1000000
Set<Int?>.build                   1292.000 ns ±  11.42 %    1000000
Slice<Array<Any>>.build           2209.000 ns ±   4.95 %     629537
Slice<Array<Int>>.build            584.000 ns ±  17.10 %    1000000
Slice<Array<Int?>>.build           917.000 ns ±   8.49 %    1000000
String.build                       500.000 ns ±   8.91 %    1000000
String.UnicodeScalarView.build    3958.000 ns ±   3.04 %     351918
String.UTF8View.build              542.000 ns ±  10.33 %    1000000
Substring.build                   1709.000 ns ±   4.41 %     810685
Substring.UnicodeScalarView.build 5084.000 ns ±   3.19 %     274560
Substring.UTF8View.build          1333.000 ns ±   5.89 %    1000000