Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
In the last episode we explored how functional setters allow us to dive deeper into nested structures to perform transformations while leaving everything else in the structure fixed. We played around with some toy examples, like nested tuples and arrays, and we showed off some pretty impressive stuff, but at the end of the day we aren’t typically transforming tuples. Instead, we have real world data with structs. We want to bring all the ideas from the previous episode into the world of structs so that we can transform a deeply nested struct in a simple, expressive manner. To do this, we are going to leverage Swift’s key paths!
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
In this episode we used
Dictionary
’s subscript key path without explaining it much. For akey: Key
, one can construct a key path\.[key]
for setting a value associated withkey
. What is the signature of the setterprop(\.[key])
? Explain the difference between this setter and the setterprop(\.[key]) <<< map
, wheremap
is the optional map.Solution
The signature of the setter
prop(\.[key])
is((Value?) -> Value?) -> [Key: Value] -> [Key: Value]
, whereas the signature for the mapped version is((Value) -> Value) -> [Key: Value] -> [Key: Value]
. Theprop(\.[key])
version allows you to both set values for nonexistant keys as well as nil out existing keys.The
Set<A>
type in Swift does not have any key paths that we can use for adding and removing values. However, that shouldn’t stop us from defining a functional setter! Define a functionelem
with signature(A) -> ((Bool) -> Bool) -> (Set<A>) -> Set<A>
, which is a functional setter that allows one to add and remove a valuea: A
to a set by providing a transformation(Bool) -> Bool
, where the input determines if the value is already in the set and the output determines if the value should be included.Solution
func elem<A>(_ e: A) -> (@escaping (Bool) -> Bool) -> (Set<A>) -> Set<A> { return { shouldInclude in return { set in if shouldInclude(set.contains(e)) { return set.union(Set([e])) } else { return set.subtracting(Set([e])) } } } } let xs: Set<String> = [1, 2, 3, 4] xs |> elem(1) { _ in false } |> elem(2) { !$0 } |> elem(10) { _ in true }
Generalizing exercise #1 a bit, it turns out that all subscript methods on a type get a compiler generated key path. Use array’s subscript key path to uppercase the first favorite food for a user. What happens if the user’s favorite food array is empty?
Solution
(prop(\User.favoriteFoods[0].name)) { $0.uppercased() }
If the user’s favorite food array is empty, an out of bounds error will be thrown.Recall from a previous episode that the free
filter
function on arrays has the signature((A) -> Bool) -> ([A]) -> [A]
. That’s kinda setter-like! What does the composed setterprop(\User.favoriteFoods) <<< filter
represent?Solution
prop(\User.favoriteFoods) <<< filter
allows you to filter a users favorite foods in-place. For example, the following code, updates a user to keep only fruits that start with ‘o’.user |> (prop(\User.favoriteFoods) <<< filter) { $0.name.starts(with: 'o') }
Define the
Result<Value, Error>
type, and createvalue
anderror
setters for safely traversing into those cases.Solution
func value<A, E>(_ f: @escaping (A) -> B) -> (Result<A, E>) -> Result<B, E> { return { result in switch result { case .success(let v): return .success(f(v)) case .failure(let e): return .failure(e) } } } func error<A, E>(_ f: @escaping (E) -> F) -> (Result<A, E>) -> Result<A, F> { return { result in switch result { case .success(let v): return .success(v) case .failure(let e): return .failure(f(e)) } } }
Is it possible to make key path setters work with
enum
s?Solution
Unfortunately no :( Swift only gives us access to key paths for structs, and provides nothing for enums. Maybe that will change some day!
Redefine some of our setters in terms of
inout
. How does the type signature and composition change?Solution
func inoutProp<Root, Value>(_ kp: WritableKeyPath<Root, Value>) -> (@escaping (inout Value) -> Void) -> (inout Root) -> Void { return { update in { root in update(&root[keyPath: kp]) } } }
References
Composable Setters
Stephen Celis • Saturday Sep 30, 2017Stephen spoke about functional setters at the Functional Swift Conference if you’re looking for more material on the topic to reinforce the ideas.
Semantic editor combinators
Conal Elliott • Monday Nov 24, 2008Conal Elliott describes the setter composition we explored in this episode from first principles, using
Haskell. In Haskell, the backwards composition operator <<<
is written simply as a dot .
, which means
that g . f
is the composition of two functions where you apply f
first and then g
. This means if had
a nested value of type ([(A, B)], C)
and wanted to create a setter that transform the B
part, you would
simply write it as first.map.second
, and that looks eerily similar to how you would field access in
the OOP style!