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

Container types and how to use them

Container types and how to use them

What does Optional, Array, Result and Observable have in common? Why was Array’s ‘flatMap’ changed to ’compactMap’? Can you zip Results? What is ‘sequence’ function? This talks will answer these questions and explain the mental model of containers that’s behind various widely-used Swift types. Once you see how they relate to each other, we’ll explore what you can do with them and how to handle the issues that arive when you model your code in a container fashion.

Krzysztof Siejkowski

April 17, 2019
Tweet

More Decks by Krzysztof Siejkowski

Other Decks in Programming

Transcript

  1. Container types
    and how to use them
    Krzysztof Siejkowski

    View Slide

  2. Questions
    are welcome!

    View Slide

  3. Container
    Container
    A Container holds 

    the instance 

    of type A
    while adding

    its context
    instance of type A
    context

    View Slide

  4. View Slide

  5. Almost
    5 years
    ago…

    View Slide

  6. Optional
    Optional
    A ∅
    instance of type A
    lack of
    instance of type A
    context:
    possible
    lack of value

    View Slide

  7. Result
    Result
    A E
    context:
    possible error
    instance of type A
    failure
    (usually error)

    View Slide

  8. Array
    Array
    context:
    possible
    multiple values
    some more possible
    instances of type A
    A
    A
    A
    A
    A
    A
    A
    A
    A
    A
    A
    A
    possible instance of type A

    View Slide

  9. View Slide

  10. Generator AKA Lazy
    Lazy
    context:
    deferred
    evaluation
    of value
    block that
    evaluates value
    when needed
    () -> A

    View Slide

  11. Future
    Future
    context:
    asynchronous
    computation
    of value
    block that provides value
    once it becomes available
    block that
    kicks off
    computation
    -> Void
    -> Void
    A

    View Slide

  12. Either
    Either
    A B
    context:
    one of
    two values
    instance of type A
    instance of type B

    View Slide

  13. Observable
    Observable
    context:
    sequence
    of events
    in time
    block that
    generates events
    (might be called
    multiple times)
    () -> Event

    View Slide

  14. Mental model
    Container
    A Container holds 

    the instance 

    of type A
    while adding

    its context
    instance of type A
    context

    View Slide

  15. View Slide

  16. flatMap
    A
    (A) ->
    Container
    B
    A
    Container
    + (A) -> Container
    = Container

    View Slide

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

    View Slide

  18. !"

    View Slide

  19. Container
    + (A) -> Void
    = Container
    A
    (A) -> Void
    A
    do
    A

    View Slide

  20. reduce
    A A
    Context related
    side-effect
    Container => A or side-effect

    View Slide

  21. Container
    + (A) -> Void
    = Void
    A
    (A) -> Void
    A
    forEach

    View Slide

  22. zip
    C
    A
    B
    A
    B (A, B) -> C
    (Container, Container)
    + (A, B) -> C
    => Container
    C

    View Slide

  23. unzip
    C
    A
    B
    A
    B
    C -> (A, B)
    Container
    + (C) -> (A, B)
    => (Container,
    Container)
    C

    View Slide

  24. !"

    View Slide

  25. flatMap compactMap
    A
    (A) ->
    Optional
    B
    A
    Container
    + (A) -> Optional
    = Container
    B
    B

    View Slide

  26. flatMap
    A
    Container
    + (A) -> Container
    = Container
    A
    (A) ->
    Container
    B
    A

    View Slide

  27. View Slide

  28. What is a context?
    Container
    A Container holds 

    the instance 

    of type A
    while adding

    its context
    instance of type A
    context

    View Slide

  29. Context describes
    the constraints of
    your logic

    View Slide

  30. • nil / null
    • failure / error / exception
    • lazy evaluation
    • unknown number of values
    • asynchronous operation
    • … many more!

    View Slide

  31. View Slide

  32. View Slide

  33. View Slide

  34. let interestingData = networkClient
    .fetchData()
    .flatMap { validateStatusCode($0) }
    .flatMap { deserialize($0) }
    .map { getSubsetOfData($0) }
    .reduce { handleSideEffect($0) }

    View Slide

  35. View Slide

  36. Scott Wlaschin, “Railway Oriented Programming”
    https://www.slideshare.net/ScottWlaschin/railway-oriented-programming

    View Slide

  37. Scott Wlaschin, “Railway Oriented Programming”
    https://www.slideshare.net/ScottWlaschin/railway-oriented-programming

    View Slide

  38. Many contexts
    One API

    View Slide

  39. optionalValue
    .map { ... }
    .flatMap { ... }
    asynchronousValue
    .map { ... }
    .flatMap { ... }
    multipleValues
    .map { ... }
    .flatMap { ... }
    possibleErrorValue
    .map { ... }
    .flatMap { ... }

    View Slide

  40. View Slide

  41. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. 2. Deserialize
    Response is a JSON object in form of an array of repos:
    [
    {
    “name”: “repo_name”,
    ...
    },
    ...
    deserialize: (Data) throws -> [Data]
    throws -> [Repo]

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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




    View Slide

  52. View Slide

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

    View Slide

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

    View Slide

  55. Validated
    A
    E
    E E
    E
    E
    E
    Result
    A E

    View Slide

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

    View Slide

  57. sequence
    A
    A
    Container1> => Container2>

    View Slide

  58. Given: 

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

    View Slide

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

    View Slide

  60. flatten
    A
    A
    Container> => Container

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

  72. Method-based
    1. Generalize the method,

    not extension
    2. Add constraints to 

    the method, not extension

    View Slide

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

    View Slide

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

  75. !"

    View Slide

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

    View Slide

  77. View Slide

  78. Use code generation
    Please see this great talk for details:

    Elviro Rocca — Protocol-Oriented Monad Transformers
    https://www.youtube.com/watch?v=Zmb86zblcto

    View Slide

  79. Use “wide”
    containers,
    like Observable

    View Slide

  80. Future< <== observable is async
    Validated< <== observable can return error
    Array< <== observable is sequence
    Repo
    >,
    Either
    >
    >
    Observable <- but some information will be lost!

    View Slide

  81. View Slide

  82. Thanks!
    Questions?

    View Slide