$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. View Slide

  3. What is container?
    Container
    A
    instance of type A
    Container holds 

    the instance 

    of type A
    while adding

    its context

    View Slide

  4. enum Optional {
    case some(A)
    case none
    }
    Optional
    Optional
    A ∅

    View Slide

  5. Result
    A E

    View Slide

  6. Either
    A B
    Result
    A E

    View Slide

  7. Either
    A B
    Result
    A E
    Future
    ((A) -> Void) -> Void

    View Slide

  8. Either
    A B
    Result
    A E
    Future
    ((A) -> Void) -> Void
    Observable
    () -> Event
    (A) -> Void

    View Slide

  9. View Slide

  10. View Slide

  11. Container + (A) -> B = Container
    map
    A (A) -> B B

    View Slide

  12. forEach
    A (A) -> Void

    View Slide

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

    View Slide

  14. forEach
    A (A) -> Void
    A (A) ->
    Container
    B
    flatMap
    A
    A
    reduce

    View Slide

  15. View Slide

  16. As a user, I want to see
    all my Github repos
    written in Swift language

    View Slide

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

    View Slide

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

    View Slide

  19. View Slide

  20. 1. Fetch data from Github
    extension URLSession {
    func dataTask(
    with url: URL,
    completionHandler:
    @escaping (Data?, URLResponse?, Error?) -> Void
    ) -> URLSessionDataTask
    }

    View Slide

  21. 1. Fetch data from Github
    completionHandler
    => Future
    (Data?, URLResponse?, Error?)
    => Result
    Future>

    View Slide

  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]

    View Slide

  23. 2. Deserialize
    deserialize: (Data) throws -> [Data]
    throws -> [Repo]
    => (Data) -> Result<[Data], DeserializationError>
    -> Result<[Result],
    DeserializationError>
    FutureResult<
    [Result],
    DeserializationError
    >,
    NetworkError
    >>

    View Slide

  24. 3. Filter Swift repos
    filter: (Repo) -> Bool
    repos.filter { (repo: Repo) -> Bool in
    repo.language == "Swift"
    }
    "Swift"

    View Slide

  25. 3. Filter Swift repos
    let repos: FutureResult<
    [Result],
    DeserializationError
    >,
    NetworkError
    >> = githubClient.fetch(.repos, for: user)
    .map { deserialize($0) }
    repos.filter { }

    View Slide

  26. Future<
    Result<
    Result<
    Array<
    Result<
    Repo,
    DeserializationError
    >
    >,
    DeserializationError
    >,
    NetworkError
    >
    >




    View Slide

  27. Simplifying
    by
    transforming

    View Slide

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

    View Slide

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

    View Slide

  30. Validated
    A
    E
    E E
    E
    E
    E
    Result
    A E

    View Slide

  31. extension Result {
    func asValidated() -> Validated { ... }
    }
    Future<
    Validated<
    Validated<
    Array>,
    DeserializationError
    >,
    NetworkError
    >
    >

    View Slide

  32. sequence
    A
    A
    Container1> => Container2>

    View Slide

  33. Given: 

    Array>
    Sequence:
    If any element of array is nil, then return nil
    Else, return array of unwrapped, non-nil elements
    Then:
    Optional>

    View Slide

  34. func sequence(_ vs: [Validated]) -> Validated<[A], E>
    Future<
    Validated<
    Validated<
    Validated<
    Array,
    DeserializationError
    >,
    DeserializationError
    >,
    NetworkError
    >
    >

    View Slide

  35. Container> => Container
    flatten
    A
    A

    View Slide

  36. extension Validated {
    func flatten() -> Validated
    where A == Validated { ... }
    }
    Future<
    Validated<
    Validated<
    Array,
    DeserializationError
    >,
    NetworkError
    >
    >

    View Slide

  37. extension Validated {
    func mapErrors(_ f: (E) -> F) -> Validated { ... }
    }
    // put errors into Either container
    Future<
    Validated<
    Validated<
    Array,
    Either
    >,
    Either
    >
    >

    View Slide

  38. We’re left with 3 containers-deep nesting
    // after flattening again
    Future< // 1st level
    Validated< // 2nd level
    [Repo], // 3rd level
    Either
    >
    >

    View Slide

  39. Filter Swift repos
    let repos: Future<
    Validated<
    Array,
    Either
    >
    > = githubClient.fetch(.repos, for: user)
    .map { deserialize($0) }
    .map { // transforms described before }
    repos.filter { }

    View Slide

  40. Write “nested”
    methods for
    containers

    View Slide

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

    View Slide

  42. Two ways of
    implementing:
    • protocol-based
    • method-based

    View Slide

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

    : Protocol
    struct Container
    “Type rasure”
    protocol ContainerType
    { associatedtype A }

    View Slide

  44. 1. Create protocol
    protocol ValidatedType {
    associatedtype PA
    associatedtype PE
    }
    extension Validated: ValidatedType {
    typealias PA = A
    typealias PE = E
    }

    View Slide

  45. 2. Duplicate API in protocol (return concrete type)
    protocol ValidatedType {
    associatedtype PA
    associatedtype PE
    func map(_ f: (PA) -> B) -> Validated
    }
    extension Validated: ValidatedType {}

    View Slide

  46. 3. Implement the nested method in a constrained extension
    extension Future where A: ValidatedType {
    func map(
    _ f: @escaping (A.PA) -> B
    ) -> Future> {
    return map { $0.map(f) }
    }
    }

    View Slide

  47. Method-based
    1. Generalize the method,

    not extension
    2. Add constraints to 

    the method, not extension

    View Slide

  48. 1. Generalize the method, not extension
    extension Future {
    func map(
    _ f: @escaping (VA) -> B
    ) -> Future> {
    // work in progress
    }
    }

    View Slide

  49. 2. Add constraints to the method, not extension
    extension Future {
    func map(
    _ f: @escaping (VA) -> B
    ) -> Future>
    where A == Validated {
    return map { $0.map(f) }
    }
    }

    View Slide

  50. Now we can filter Swift repos!
    let repos: Future<
    Validated<
    Array,
    Either
    >> = githubClient.fetch(.repos, for: user)
    .map { deserialize($0) }
    .map { // transforms described before }
    repos.filter { (repo: Repo) -> Bool in
    repo.language == “Swift”
    } // it works!

    View Slide

  51. A lot of 

    boilerplate?

    View Slide

  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

    View Slide

  53. Use “wide”
    container, like

    Observable

    View Slide

  54. Future< <== observable is async
    Validated< <== observable can return error
    Array< <== observable is sequence
    Repo
    >,
    Either
    >
    >

    View Slide

  55. View Slide

  56. Thanks!
    Questions?

    View Slide