cocoa heads 09112017
TRANSCRIPT
CocoaHeads@v_pradeilles | Medium | LinkedIn
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
Completion HandlersCompletion 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 })
Completion HandlersThis 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 ».
RxRx 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 })
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?
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() } }
Functional programmingCurrying 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
How to implement curryEasier 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.
Base case
func fromAsync(_ asyncRequest: @escaping ((Element) -> Void) -> Void) -> Observable<Element> { return Observable.create({ (o) -> Disposable in asyncRequest({ (result) in o.onNext(result) o.onCompleted() }) return Disposables.create() }) }
Recursion
func fromAsync<T>(_ asyncRequest: @escaping (T, (Element) -> Void) -> Void) -> (T) -> Observable<Element> { return { (a: T) in Observable.fromAsync(curry(asyncRequest)(a)) } }
func fromAsync<T, U>(_ asyncRequest: @escaping (T, U, (Element) -> Void) -> Void) -> (T, U) -> Observable<Element> { return { (a: T, b: U) in Observable.fromAsync(curry(asyncRequest)(a)(b)) } }
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 })
PerformancesNothing 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.
Currying in JSJavaScript 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 ); }; }()); }
Bonus #1Async 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")
Bonus #1Good 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") // 🎉
Bonus #1By 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! }}
Bonus #2struct 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 } }}
Bonus #2By 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 } }}
RecapWhat 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
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
Real world situationlet accountNumber: String = getAccountNumberFromUser()
// .... some code
if !isValidAccountNumber(accountNumber) { return }
// .... some more code
performBusinessLogic(with: accountNumber)
Real world situationThe 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
Model-driven securityWrap data in business objects: struct AccountNumber { let number: String init?(untrustedAccountNumber: String) { guard isValidAccountNumber(untrustedAccountNumber) else { return nil } self.number = untrustedAccountNumber }}
Model-driven securityAnd allow business services to only read data from such objects: func performBusinessLogic(with accountNumber: AccountNumber) { // ...}
That’s better, but not enoughstruct 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 }}
That’s better, but not enoughstruct 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 }}
That’s better, but not enoughThe 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.
The Validator patternpublic struct Untrusted<T> { fileprivate let value: T public init(_ value: T) { self.value = value }}
The Validator patternpublic 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}
The Validator patternextension 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!
Let’s see it in actionstruct AccountNumberValidator: Validator { static func validation(value: String) -> Bool { return isValidAccountNumber(value) }}
Let’s see it in actionstruct AccountNumberValidator: Validator { static func validation(value: String) -> Bool { return isValidAccountNumber(value) }}
struct AccountNumber { let number: String init?(untrustedAccountNumber: Untrusted<String>) { guard let validAccountNumber = AccountNumberValidator.validate(untrusted: untrustedAccountNumber) else { return nil } self.number = validAccountNumber }}
Finally, some coding guidelinesWhenever 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 ✅
Thanks, again!• Detailed explanation: https://medium.com/@vin.pradeilles/a-pattern-
to-easily-enforce-external-data-validation-in-swift-8bc1d5917bfd 👏
• For some more content about secure coding : https://www.youtube.com/watch?v=oqd9bxy5Hvc (2016 GOTO conference)