Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Matryoshka in Swift: handle nested containers with grace

Matryoshka in Swift: handle nested containers with grace

Do you like container types? I mean, the Results, Optionals, Futures, Observables? Would you feel alright if your child married one?
If the answer is yes, then how would you like a container inside a container? And what about a container inside a container inside a container? Or a container inside a container inside a container inside a container? Oh boy, things are quickly getting out of hands!
In this talk I'll show some ideas on how to work with the real-world container nesting and the tradeoffs that are coming along.

Krzysztof Siejkowski

November 19, 2018
Tweet

More Decks by Krzysztof Siejkowski

Other Decks in Technology

Transcript

  1. Matryoshka 
 in Swift Krzysztof Siejkowski, @_siejkowski

  2. None
  3. What is container? Container<A> A instance of type A Container

    holds 
 the instance 
 of type A while adding
 its context
  4. enum Optional<A> { case some(A) case none } Optional Optional<A>

    A ∅
  5. Result<A, E> A E

  6. Either<A, B> A B Result<A, E> A E

  7. Either<A, B> A B Result<A, E> A E Future<A> ((A)

    -> Void) -> Void
  8. Either<A, B> A B Result<A, E> A E Future<A> ((A)

    -> Void) -> Void Observable<A> () -> Event<A> (A) -> Void
  9. None
  10. None
  11. Container<A> + (A) -> B = Container<B> map A (A)

    -> B B
  12. forEach A (A) -> Void

  13. forEach A (A) -> Void A A reduce

  14. forEach A (A) -> Void A (A) -> Container<B> B

    flatMap A A reduce
  15. None
  16. As a user, I want to see all my Github

    repos written in Swift language
  17. 1. Fetch data from Github 2. Deserialize 3. Filter Swift

    repos
  18. githubClient .fetch(.repos, for: user) .map { deserialize($0) } .filter {

    $0.isSwiftRepo }
  19. None
  20. 1. Fetch data from Github extension URLSession { func dataTask(

    with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void ) -> URLSessionDataTask }
  21. 1. Fetch data from Github completionHandler => Future (Data?, URLResponse?,

    Error?) => Result<Data, NetworkError> Future<Result<Data, NetworkError >>
  22. 2. Deserialize Result is a JSON object in form of

    an array of repos: [ { “name”: “repo_name”, ... }, ... We want to know which ones of the repos couldn’t be deserialized deserialize: (Data) throws -> [Data] throws -> [Repo]
  23. 2. Deserialize deserialize: (Data) throws -> [Data] throws -> [Repo]

    => (Data) -> Result<[Data], DeserializationError> -> Result<[Result<Repo, DeserializationError>], DeserializationError> Future<Result< Result< [Result<Repo, DeserializationError>], DeserializationError >, NetworkError >>
  24. 3. Filter Swift repos filter: (Repo) -> Bool repos.filter {

    (repo: Repo) -> Bool in repo.language == "Swift" } "Swift"
  25. 3. Filter Swift repos let repos: Future<Result< Result< [Result<Repo, DeserializationError>],

    DeserializationError >, NetworkError >> = githubClient.fetch(.repos, for: user) .map { deserialize($0) } repos.filter { }
  26. Future< Result< Result< Array< Result< Repo, DeserializationError > >, DeserializationError

    >, NetworkError > > ❌
  27. Simplifying by transforming

  28. What’s the difference? Optional<A> A ∅ Either<A, Void> A Void

  29. You can write a transform! Optional<A> A ∅ Either<A, Void>

    A Void Optional<A> A ∅ Either<A, Void> A Void
  30. Validated<A, E> A E E E E E E Result<A,

    E> A E
  31. extension Result { func asValidated() -> Validated<A, E> { ...

    } } Future< Validated< Validated< Array<Validated<Repo, DeserializationError >>, DeserializationError >, NetworkError > >
  32. sequence A A Container1<Container2<A >> => Container2<Container1<A >>

  33. Given: 
 Array<Optional<A >> Sequence: If any element of array

    is nil, then return nil Else, return array of unwrapped, non-nil elements Then: Optional<Array<A >>
  34. func sequence<A, E>(_ vs: [Validated<A, E>]) -> Validated<[A], E> Future<

    Validated< Validated< Validated< Array<Repo>, DeserializationError >, DeserializationError >, NetworkError > >
  35. Container<Container<A >> => Container<A> flatten A A

  36. extension Validated { func flatten<B>() -> Validated<B, E> where A

    == Validated<B, E> { ... } } Future< Validated< Validated< Array<Repo>, DeserializationError >, NetworkError > >
  37. extension Validated { func mapErrors<F>(_ f: (E) -> F) ->

    Validated<A, F> { ... } } // put errors into Either container Future< Validated< Validated< Array<Repo>, Either<NetworkError, DeserializationError> >, Either<NetworkError, DeserializationError> > >
  38. We’re left with 3 containers-deep nesting // after flattening again

    Future< // 1st level Validated< // 2nd level [Repo], // 3rd level Either<NetworkError, DeserializationError> > >
  39. Filter Swift repos let repos: Future< Validated< Array<Repo>, Either<NetworkError, DeserializationError>

    > > = githubClient.fetch(.repos, for: user) .map { deserialize($0) } .map { // transforms described before } repos.filter { }
  40. Write “nested” methods for containers

  41. Container1<Container2<A >> + (A) -> B = Container1<Container2<B >> innerMap

    B A (A) -> B
  42. Two ways of implementing: • protocol-based • method-based

  43. Inverse of type erasure protocol Protocol { associatedtype A }

    Type erasure struct AnyProtocol<A> 
 : Protocol struct Container<A> “Type rasure” protocol ContainerType { associatedtype A }
  44. 1. Create protocol protocol ValidatedType { associatedtype PA associatedtype PE

    } extension Validated: ValidatedType { typealias PA = A typealias PE = E }
  45. 2. Duplicate API in protocol (return concrete type) protocol ValidatedType

    { associatedtype PA associatedtype PE func map<B>(_ f: (PA) -> B) -> Validated<B, PE> } extension Validated: ValidatedType {}
  46. 3. Implement the nested method in a constrained extension extension

    Future where A: ValidatedType { func map<B>( _ f: @escaping (A.PA) -> B ) -> Future<Validated<B, A.PE >> { return map { $0.map(f) } } }
  47. Method-based 1. Generalize the method,
 not extension 2. Add constraints

    to 
 the method, not extension
  48. 1. Generalize the method, not extension extension Future { func

    map<VA, VE, B>( _ f: @escaping (VA) -> B ) -> Future<Validated<B, VE >> { // work in progress } }
  49. 2. Add constraints to the method, not extension extension Future

    { func map<VA, VE, B>( _ f: @escaping (VA) -> B ) -> Future<Validated<B, VE >> where A == Validated<VA, VE> { return map { $0.map(f) } } }
  50. Now we can filter Swift repos! let repos: Future< Validated<

    Array<Repo>, Either<NetworkError, DeserializationError> >> = githubClient.fetch(.repos, for: user) .map { deserialize($0) } .map { // transforms described before } repos.filter { (repo: Repo) -> Bool in repo.language == “Swift” } // it works!
  51. A lot of 
 boilerplate?

  52. Use code generation (like Sourcery) Please see this great talk

    for details:
 Elviro Rocca — Protocol-Oriented Monad Transformers https://www.youtube.com/watch?v=Zmb86zblcto
  53. Use “wide” container, like
 Observable

  54. Future< <== observable is async Validated< <== observable can return

    error Array< <== observable is sequence Repo >, Either<NetworkError, DeserializationError> > >
  55. None
  56. Thanks! Questions?