Swift Result Builders: Getting Started
Adding @resultBuilder in Swift 5.4 was important, but you might have missed it. It’s the secret engine behind the easy syntax you use to describe a view’s layout: @ViewBuilder. If you’ve ever wondered whether you could create custom syntax like that in your projects, the answer is yes! Even better, you’ll be amazed at how […] By Andrew Tetlaw.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Result Builders: Getting Started
20 mins
- Getting Started
- Introducing Decoder Ring
- Making Your First Result Builder
- Understanding Result Builders
- Planning Your Cipher Builder
- Defining a Cipher Rule
- Writing the Rules
- Building a Cipher
- Expanding Syntax Support
- Understanding Result Builder Loops
- Adding Support for Optional Values
- Where to Go From Here?
Understanding Result Builder Loops
How does that loop work? Add a breakpoint inside both result builder functions (by clicking the line numbers). Build and run.
When you type a letter, you can see each step. Each time execution stops, click the continue button to jump to the next breakpoint until it's finished.
You'll find that the compiler hits the buildBlock
three times, the buildArray
once, and then the buildBlock
one last time. You can imagine the compiler creating something like this:
// 1
let rule1: CipherRule = CipherBuilder.buildBlock(
LetterSubstitution(offset: 7)
)
let rule2: CipherRule = CipherBuilder.buildBlock(
LetterSubstitution(offset: 7)
)
let rule3: CipherRule = CipherBuilder.buildBlock(
LetterSubstitution(offset: 7)
)
// 2
let rule4: CipherRule = CipherBuilder.buildArray(
[rule1, rule2, rule3]
)
- This is where you loop three times. The result builder calls
buildBlock(_:)
each time to output a single rule. In this case, the rule is an instance ofLetterSubstitution
. - The result builder assembles these three rules into a single array and calls
buildArray(_:)
. Once again, the result is output as a single rule. - Finally, the result builder calls
buildBlock(_:)
again to return that rule as the result.
You'll never see this code anywhere, but imagining what's happening internally when you plan a result builder is helpful. It's all in the planning and your use of CipherRule
as the primary type that's paid off handsomely. Nice work, Agent.
Adding Support for Optional Values
Okay...so now those eggheads are scrambling to produce an even stronger cipher. They feel it's unwise to allow official terminology to be output in the cipher text. So they would like to optionally supply a dictionary of official terms and an obfuscated replacement. Like swapping "brains" for "Swiss cheese", you muse.
It's time for another CipherRule
!
Create a file called ReplaceVocabulary.swift and add:
struct ReplaceVocabulary: CipherRule {
// 1
let terms: [(original: String, replacement: String)]
func encipher(_ value: String) -> String {
// 2
terms.reduce(value) { encipheredMessage, term in
encipheredMessage.replacingOccurrences(
of: term.original,
with: term.replacement,
options: .caseInsensitive
)
}
}
func decipher(_ value: String) -> String {
// 3
terms.reduce(value) { decipheredMessage, term in
decipheredMessage.replacingOccurrences(
of: term.replacement,
with: term.original,
options: .caseInsensitive
)
}
}
}
-
terms
is an array of tuples with twoString
s each, matching the original term with its replacement. - In
encipher(_:)
, you loop through the array and perform the replacements in a case-insensitive manner. -
decipher(_:)
does the same but swaps all the replacements with originals.
Open SuperSecretCipher.swift. Add this property to let the eggheads control the optionality:
let useVocabularyReplacement: Bool
It's a simple Bool
that you now need to use in cipherRule
. Add the following before the cycles
loop:
if useVocabularyReplacement {
ReplaceVocabulary(terms: [
("SECRET", "CHOCOLATE"),
("MESSAGE", "MESS"),
("PROTOCOL", "LEMON GELATO"),
("DOOMSDAY", "BLUEBERRY PIE")
])
}
The idea is that, for a message such as "the doomsday protocol is initiated", your cipher will first replace it with "the BLUEBERRY PIE LEMON GELATO is initiated" before the letter substitution occurs. This will surely confound enemy spies!
If you build and run the app, you see a familiar build error:
This time, open CipherBuilder.swift. Add the following method to CipherBuilder
:
static func buildOptional(_ component: CipherRule?) -> CipherRule {
component ?? []
}
This is how result builders handle optionality, such as an if
statement. This one calls buildOptional(_:)
with a CipherRule
or nil
, depending on the condition.
How can the fallback value for CipherRule
be []
? This is where you take advantage of the Swift type system. Because you extended Array
to be a CipherRule
when the element type is CipherRule
, you can return an empty array when component
is nil
. You could expand that function body to express these types explicitly:
let fallback: [CipherRule] = .init(arrayLiteral: [])
return component ?? fallback
But you're in the business of allowing the compiler to just do its thing. :]
In your result builder's design, that empty array will not affect the result, which is precisely what you're looking for in the if useVocabularyReplacement
expression. Pretty smart, Agent. That's the sort of on-your-feet thinking that'll get HQ's attention...and maybe that promotion?
Open ContentView.swift. Update cipher
inside processMessage(_:)
to take in the new useVocabularyReplacement
parameter:
let cipher = SuperSecretCipher(
offset: 7,
cycles: 3,
useVocabularyReplacement: true
)
Build and run to see how your SuperSecretCipher
performs.
Perfect! The eggheads are finally satisfied, and your presence is required at HQ. On to the next mission, Agent, and remember that result builders are at your disposal.
Where to Go From Here?
You've only begun to explore the possibilities of result builders. You can find information about additional capabilities in the documentation:
For inspiration, you might want to check out Awesome result builders, a collection of result builders you can find on GitHub.
If you're looking for an extra challenge, try implementing support for if { ... } else { ... }
statements and other result builder logic. Or check out this list of historical ciphers at Practical Cryptography and pick one to form a new CipherRule
. You'll find a couple of familiar entries in that list. :]
I hope you enjoyed this tutorial on result builders. If you have any questions or comments, please join the forum discussion below.