Skip to content

Commit fa2bdb4

Browse files
authored
Merge pull request #46 from nshint/post/curry
Add curry post
2 parents 00d09b9 + c3a5b94 commit fa2bdb4

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
layout: post
3+
author: rafa
4+
title: "Complete flows, partial models"
5+
date: 2019-07-15 19:55:56 +0200
6+
comments: true
7+
categories:
8+
---
9+
10+
Most apps these days have a sequence of screens that gather information from the user, like a registration flow, a form of some kind. The data from each step is typically combined into a single data structure.
11+
For example, let's say we want the name, age, and the password to authenticate the user.
12+
<!--more-->
13+
One way to model it is by using the following data structure:
14+
15+
```swift
16+
struct FormData {
17+
let name: String
18+
let age: Int
19+
let password: String
20+
}
21+
```
22+
23+
One issue we are going to come about is that our model is strict, it needs all the values at once, whereas users will supply each value at a time. First they will type in their name, then their age, and so on.
24+
Wrapping up the fields in `Optional`, may loosen its strictness.
25+
26+
```swift
27+
struct FormData {
28+
let name: String?
29+
let age: Int?
30+
let password: String?
31+
}
32+
```
33+
34+
Our flow code might look like:
35+
36+
```swift
37+
func firstStepFinished(with name: String) -> FormData {
38+
return FormData(name: name, age: nil, password: nil)
39+
}
40+
41+
func secondStepFinished(with age: Int, partialFormData: FormData) -> FormData {
42+
return FormData(name: partialFormData.name, age: age, password: nil)
43+
}
44+
45+
func thirdStepFinished(with password: String, partialFormData: FormData) {
46+
let formData = FormData(name: partialFormData.name, age: partialFormData.age, password: password)
47+
48+
api.performLogin(with: formData)
49+
}
50+
```
51+
52+
However, now we need to `guard` against any `nil` values if we want to use them (for example, to make a network request).
53+
54+
```swift
55+
guard let name = formData.name,
56+
let age = formData.age,
57+
let password = formData.password {
58+
return // what should we do here???
59+
}
60+
61+
// use data
62+
```
63+
64+
From a domain perspective, that `return` doesn't make any sense.
65+
66+
One could argue that it's "save" to force unwrap in this case, or that there are [already a nice approach to this problem](https://www.swiftbysundell.com/posts/handling-non-optional-optionals-in-swift).
67+
68+
One may say, _"we can raise an error to the user"_ or _"we could track it and check if users are getting stuck somehow"_. But, at the end of the day, this is not a good solution because you know that when the flow ends, you have all the values.
69+
70+
Our model is "lying" to us. That's not loosen, it's just flawed.
71+
72+
There are several approaches to make it better, like "one model per step":
73+
74+
```swift
75+
struct FirstStep {
76+
let name: String
77+
}
78+
79+
struct SecondStep {
80+
let name: String
81+
let age: Int
82+
83+
init(firstStep: FirstStep, age: Int) {
84+
self.name = firstStep.name
85+
self.age = age
86+
}
87+
}
88+
89+
struct ThirdStep {
90+
let name: String
91+
let age: Int
92+
let password: String
93+
94+
init(secondStep: SecondStep, password: String) {
95+
self.name = secondStep.name
96+
self.age = secondStep.age
97+
self.password = password
98+
}
99+
}
100+
```
101+
102+
That's better! But there is also another way of doing things that doesn't involve duplication nor partial data structs.
103+
104+
Instead of breaking down our data structure, why not to break down functions?
105+
106+
Our `FormData` initializer, when interpreted as a function, has this shape:
107+
108+
```swift
109+
(String, Int, String) -> FormData
110+
```
111+
112+
But we can break it down into plain old lambdas[^1], and by applying it to the initializer for our data structure:
113+
114+
```swift
115+
(String) -> (Int) -> (String) -> FormData
116+
```
117+
118+
This technique is called [currying](https://www.pointfree.co/episodes/ep5-higher-order-functions#t42). What it does is, it allow us to translate the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument.
119+
120+
```swift
121+
func curry<A, B, C, D>(
122+
_ f: @escaping (A, B, C) -> D
123+
) -> (A) -> (B) -> (C) -> D {
124+
return { a in { b in { c in return f(a, b, c) } } }
125+
}
126+
```
127+
128+
The function above goes from a function that takes multiple arguments `(A, B, C)` and produces a `D`, to single functions, that take one argument each: `(A) -> (B) -> (C)` and produces a `D`, making it possible to partially apply each argument, one at the time, until it can evaluate and return the output value.
129+
130+
Using it in our flow, may look like the following:
131+
132+
```swift
133+
typealias Name = String
134+
typealias Age = Int
135+
typealias Password = String
136+
137+
typealias FromFirstStep = (Age) -> (Password) -> FormData
138+
typealias FromSecondStep = (Password) -> FormData
139+
140+
func firstStepFinished(with name: String) -> FromFirstStep {
141+
let curried = curry(FormData.init) // (Name) -> (Age) -> (Password) -> FormData
142+
return curried(name) // (Age) -> (Password) -> FormData
143+
}
144+
145+
func secondStepFinished(with age: Int, partialData: FromFirstStep) -> FromSecondStep {
146+
return partialData(age) // (Password) -> FormData
147+
}
148+
149+
func thirdStepFinished(with password: String, partialData: FromSecondStep) {
150+
let formData = partialData(password)
151+
152+
api.performLogin(with: formData)
153+
}
154+
```
155+
156+
I've added a few `type aliases` just to make it more readable.
157+
Cleaning up them further, we'll have:
158+
159+
```swift
160+
typealias FromThirdStep = FormData // just to be explicit
161+
typealias FromSecondStep = (Password) -> FromThirdStep
162+
typealias FromFirstStep = (Age) -> FromSecondStep
163+
```
164+
165+
If you ask me, this is much better because we didn't have to write anything else, other than the `curry`[^2] function itself, which can be used in other places.
166+
167+
And that's it! Functions have saved the day :)
168+
169+
P.S: I want to thank [Sean Olszewski](https://twitter.com/__chefski__), [Gordon Fontenot](https://twitter.com/gfontenot), [Peter Tomaselli](https://github.com/peter-tomaselli), [Henrique Morbin](https://twitter.com/morbin_), [Marcelo Gobetti](https://twitter.com/mwgobetti) and [João Rutkoski](https://github.com/joaortk) for their awesome review.
170+
171+
[^1]: `functions take one argument and return one result.` From the book: [`Haskell Programming from First Principles`](http://haskellbook.com/)
172+
173+
[^2]: Or just use the [Curry.framework](https://github.com/thoughtbot/Curry)

0 commit comments

Comments
 (0)