4. Swift
Quick recap on how functions are typed:
// a function that takes an Int and returns Void
let f: (Int) -> Void
// a function that takes an Int and returns a function that takes a String
and returns Void
let g: (Int) -> (String) -> Void
g(2)("Foo")
// and so on…
let h: (T) -> (U) -> (V) -> W
6. Completion Handlers
Completion handlers are an easy way to pass data asynchronously
// a function that takes a completion handler
func request(arg1: String, arg2: Int, completionHandler: @escaping (String) -> Void)
// and how it is actually called
request(arg1: "Foo", arg2: 0, completionHandler: { result in
// do something with result
})
7. Completion Handlers
This pattern is great, until you need to chain calls.
When you do, it leads to nested callbacks, which are hard to read and
reason about.
This situation is often known as « callback hell ».
9. Rx
Rx is a library that allows to write asynchronous code through the use
of Observables.
// same function, written using RxSwift
func request(arg1: String, arg2: Int) -> Observable<String>
// and you use it like this
request("Foo", 42).subscribe(onNext: { result in
// do something with result
})
10. Rx makes it easier to work with asynchronous code, so what about
converting legacy or vendor API that uses completion handlers to new
API that return Observables?
11. We can try doing it by hand:
func observableRequest(arg1: String, arg2: Int) -> Observable<String> {
return Observable.create { (observer) -> Disposable in
request(arg1: arg1, arg2: arg2, completionHandler: { result in
observer.onNext(result)
observer.onCompleted()
})
return Disposables.create()
}
}
14. Functional programming
Currying is a fundamental primitive of functional programming.
It allows us to mangle with a function signature.
// this is how you call a standard function
add(arg1: 2, arg2: 3) // returns 5
// this is how you call a curried function
let curriedAdd = curry(add(arg1:arg2:))
curriedAdd(2)(3) // returns 5
15. How to implement curry
Easier than it seems:
func curry<A, B, C>(_ function: @escaping (A, B) -> C) -> (A) -> (B) -> C {
return { (a: A) -> (B) -> C in { (b: B) -> C in function(a, b) } }
}
Basically, we wrap the function in a succession of lambdas that will
each receive one of the argument before finally calling the wrapped
function.
19. How to use it
// let’s bridge this function
func request(arg1: String, arg2: Int, completionHandler: @escaping (String) -> Void)
fromAsync(request(arg1:arg2:completionHandler:))("Foo", 3)
.subscribe(onNext: { (result) in
// do something with result
})
20. Performances
Nothing comes for free and this technique comes at some performance costs.
Still, there’s nothing we can’t manage.
• Currying function that takes many parameters (> 6) can take sometime to
compile, due to generics and type inference.
=> The bridging code can be placed in a compiled dependency
• Recursive approaches can reduce performances when applied to resource-
demanding code.
=> Profile execution to identify what is running slow, and do the bridging
manually (via boiler plate code) if needed.
22. Currying in JS
JavaScript exposes primitives that allow to manipulate more directly
the arguments list, thus allowing for a real curry function.
function curry( fn ) {
var arity = fn.length;
return (function resolver() {
var memory = Array.prototype.slice.call( arguments );
return function() {
var local = memory.slice(), next;
Array.prototype.push.apply( local, arguments );
next = local.length >= arity ? fn : resolver;
return next.apply( null, local );
};
}());
}
24. Bonus #1
Async functions (regardless of pattern) are great for UI-based code…
…but they’re a pain when it comes to units testing
func async(_ completionHandler: @escaping (String) -> Void) {
sleep(2)
completionHandler("Foo")
}
assert(/* ??? */ == "Foo")
25. Bonus #1
Good news is, Foundation has all the primitives required to convert
async to sync:
func sync() -> String {
let dispatchGroup = DispatchGroup()
var result: String? = nil
dispatchGroup.enter()
async {
result = $0
dispatchGroup.leave()
}
dispatchGroup.wait()
return result!
}
assert(sync() == "Foo") // 🎉
26. Bonus #1
By factoring out the boiler plate code, we create a new base case for
bridging:
func toSync<E>(_ async: @escaping ( @escaping (E) -> Void) -> Void) -> () -> E {
return {
let dispatchGroup = DispatchGroup()
var result: E? = nil
dispatchGroup.enter()
async {
result = $0
dispatchGroup.leave()
}
dispatchGroup.wait()
return result!
}
}
27. Bonus #2
struct MaximumValidator {
private let max: Int
private let strict: Bool
init(max: Int, strict: Bool) {
self.max = max
self.strict = strict
}
func validate(_ value: Int) -> Bool {
if strict {
return value < max
} else {
return value <= max
}
}
}
28. Bonus #2
By leveraging currying, we can store a much more relevant artifact to
perform the validation:
struct MaximumValidator {
let validate: (_ max: Int) -> Bool
init(max: Int, strict: Bool) {
self.validate = strict ? { $0 < max } : { $0 <= max }
}
}
29. Recap
What can currying bring to your code ?
• Bridge functions by operating on their signature
• Build alternate version of a function more appropriate to a given
context
• Specialize functions by binding some parameters to constant
values
31. Thanks!
• Detailed explanation: https://medium.com/@vin.pradeilles/from-
async-api-to-rx-api-with-little-to-no-work-ba725b53a1e0 👏
• Use it in production: https://github.com/RxSwiftCommunity/
RxSwiftExt#fromasync
33. A pattern to easily enforce
external data validation in Swift
34. Real world situation
let accountNumber: String = getAccountNumberFromUser()
// .... some code
if !isValidAccountNumber(accountNumber) { return }
// .... some more code
performBusinessLogic(with: accountNumber)
35. Real world situation
The validation is indeed performed… for now ⏳
But it is lost in a possibly big chunk of code, so:
- It’s easy to break 🚨
- Hard to enforce in review 🤔
➜ We need to figure out a tighter system
37. Model-driven security
Wrap data in business objects:
struct AccountNumber {
let number: String
init?(untrustedAccountNumber: String) {
guard isValidAccountNumber(untrustedAccountNumber) else { return nil }
self.number = untrustedAccountNumber
}
}
38. Model-driven security
And allow business services to only read data from such objects:
func performBusinessLogic(with accountNumber: AccountNumber) {
// ...
}
39. That’s better, but not enough
struct Transfer {
let creditAccount: String
let debitAccount: String
let amount: Int
init?(untrustedCreditAccount: String,
untrustedDebitAccount: String,
untrustedAmount: Int) {
guard isValidAccountNumber(untrustedCreditAccount),
isValidAccountNumber(untrustedCreditAccount),
isValidAmount(untrustedAmount) else { return nil }
self.creditAccount = untrustedCreditAccount
self.debitAccount = untrustedDebitAccount
self.amount = untrustedAmount
}
}
40. That’s better, but not enough
struct Transfer {
let creditAccount: String
let debitAccount: String
let amount: Int
init?(untrustedCreditAccount: String,
untrustedDebitAccount: String,
untrustedAmount: Int) {
guard isValidAccountNumber(untrustedCreditAccount),
isValidAccountNumber(untrustedCreditAccount),
isValidAmount(untrustedAmount) else { return nil }
self.creditAccount = untrustedCreditAccount
self.debitAccount = untrustedDebitAccount
self.amount = untrustedAmount
}
}
41. That’s better, but not enough
The untrusted value can still be accessed without passing the
validation.
That gives a lot of room for nasty bugs, especially in places where
copy/paste tends to be heavily relied on.
43. The Validator pattern
public struct Untrusted<T> {
fileprivate let value: T
public init(_ value: T) {
self.value = value
}
}
44. The Validator pattern
public struct Untrusted<T> {
fileprivate let value: T
public init(_ value: T) {
self.value = value
}
}
public protocol Validator {
associatedtype T
static func validation(value: T) -> Bool
}
45. The Validator pattern
extension Validator {
public static func validate(untrusted: Untrusted<T>) -> T? {
if self.validation(value: untrusted.value) {
return untrusted.value
} else {
return nil
}
}
}
We’re still in the
same file!
50. Finally, some coding guidelines
Whenever external data is retrieved, it must be immediately stored in
an Untrusted container => easy to enforce in code review ✅
Initializer of business objects can take as arguments either other
business objects or Untrusted containers, never primitive types =>
easy to enforce in code review ✅
Business logic can only operate with business objects, never with
primitive types => easy to enforce in code review ✅