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.

43d2bef703ec7165166f161f137ac54f?s=128

Krzysztof Siejkowski

November 19, 2018
Tweet

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?