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

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.

43d2bef703ec7165166f161f137ac54f?s=128

Krzysztof Siejkowski

April 17, 2019
Tweet

Transcript

  1. Container types and how to use them Krzysztof Siejkowski

  2. Questions are welcome!

  3. Container Container<A> A Container holds 
 the instance 
 of

    type A while adding
 its context instance of type A context
  4. None
  5. Almost 5 years ago…

  6. Optional Optional<A> A ∅ instance of type A lack of

    instance of type A context: possible lack of value
  7. Result Result<A, E> A E context: possible error instance of

    type A failure (usually error)
  8. Array Array<A> 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
  9. None
  10. Generator AKA Lazy Lazy<A> context: deferred evaluation of value block

    that evaluates value when needed () -> A
  11. Future Future<A> context: asynchronous computation of value block that provides

    value once it becomes available block that kicks off computation -> Void -> Void A
  12. Either Either<A, B> A B context: one of two values

    instance of type A instance of type B
  13. Observable Observable<A> context: sequence of events in time block that

    generates events (might be called multiple times) () -> Event<A>
  14. Mental model Container<A> A Container holds 
 the instance 


    of type A while adding
 its context instance of type A context
  15. None
  16. flatMap A (A) -> Container<B> B A Container<A> + (A)

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

    (A) -> B = Container<B>
  18. !"

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

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

    side-effect
  21. Container<A> + (A) -> Void = Void A (A) ->

    Void A forEach
  22. zip C A B A B (A, B) -> C

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

    Container<C> + (C) -> (A, B) => (Container<A>, Container<B>) C
  24. !"

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

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

    (A) -> Container<B> B A
  27. None
  28. What is a context? Container<A> A Container holds 
 the

    instance 
 of type A while adding
 its context instance of type A context
  29. Context describes the constraints of your logic

  30. • nil / null • failure / error / exception

    • lazy evaluation • unknown number of values • asynchronous operation • … many more!
  31. None
  32. None
  33. None
  34. let interestingData = networkClient .fetchData() .flatMap { validateStatusCode($0) } .flatMap

    { deserialize($0) } .map { getSubsetOfData($0) } .reduce { handleSideEffect($0) }
  35. None
  36. Scott Wlaschin, “Railway Oriented Programming” https://www.slideshare.net/ScottWlaschin/railway-oriented-programming

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

  38. Many contexts One API

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

    .map { ... } .flatMap { ... } multipleValues .map { ... } .flatMap { ... } possibleErrorValue .map { ... } .flatMap { ... }
  40. None
  41. None
  42. As a user, I want to see all my Github

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

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

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

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

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

    an array of repos: [ { “name”: “repo_name”, ... }, ... deserialize: (Data) throws -> [Data] throws -> [Repo]
  48. 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 >>
  49. 3. Filter Swift repos filter: (Repo) -> Bool repos.filter {

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

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

    >, NetworkError > > ❌
  52. None
  53. What’s the difference? Optional<A> A ∅ Either<A, Void> A Void

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

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

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

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

  58. Given: 
 Array<Optional<A >> When: If any element of array

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

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

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

    == Validated<B, E> { ... } } Future< Validated< Validated< Array<Repo>, DeserializationError >, NetworkError >>
  62. 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> >>
  63. We’re left with 3 containers-deep nesting // after flattening again

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

    > > = githubClient.fetch(.repos, for: user) .map { deserialize($0) } .{ // transforms described before } repos.filter { }
  65. None
  66. Container1<Container2<A >> + (A) -> B = Container1<Container2<B >> innerMap

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

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

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

    } extension Validated: ValidatedType { typealias PA = A typealias PE = E }
  70. 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 {}
  71. 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) } } }
  72. Method-based 1. Generalize the method,
 not extension 2. Add constraints

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

    map<VA, VE, B>( _ f: @escaping (VA) -> B ) -> Future<Validated<B, VE >> { // work in progress } }
  74. 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) } } }
  75. !"

  76. Now we can filter Swift repos! let repos: Future< Validated<

    Array<Repo>, Either<NetworkError, DeserializationError> >> = githubClient.fetch(.repos, for: user) .map { deserialize($0) } .{ // transforms described before } repos.filter { (repo: Repo) -> Bool in repo.language == “Swift” } // it works!
  77. None
  78. Use code generation Please see this great talk for details:


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

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

    error Array< <== observable is sequence Repo >, Either<NetworkError, DeserializationError> > > Observable<Repo> <- but some information will be lost!
  81. None
  82. Thanks! Questions?