cocoa heads 09112017

53
CocoaHeads @v_pradeilles | Medium | LinkedIn

Upload: vincent-pradeilles

Post on 22-Jan-2018

711 views

Category:

Software


0 download

TRANSCRIPT

From async to Rx, with little to no work

Swift

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

Async Programming

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 ».

Rx

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() } }

Requires a lot of boiler plate & duplicated code 😞

Functional programming

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.

Putting the pieces together

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.

It’s even easier in JS!

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

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

Questions? 🤔

💪

A pattern to easily enforce external data validation in Swift

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 security

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 pattern

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 action

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 guidelines

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 ✅

Questions? 🤔

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)

🍞🥤