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

    Container Container<A> A Container holds 
 the instance 
 of

    type A while adding
 its context instance of type A context
  2. 4.
  3. 6.

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

    instance of type A context: possible lack of value
  4. 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
  5. 9.
  6. 10.
  7. 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
  8. 12.

    Either Either<A, B> A B context: one of two values

    instance of type A instance of type B
  9. 13.

    Observable Observable<A> context: sequence of events in time block that

    generates events (might be called multiple times) () -> Event<A>
  10. 14.

    Mental model Container<A> A Container holds 
 the instance 


    of type A while adding
 its context instance of type A context
  11. 15.
  12. 16.
  13. 17.

    map A (A) -> B B A B Container<A> +

    (A) -> B = Container<B>
  14. 18.

    !"

  15. 22.

    zip C A B A B (A, B) -> C

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

    unzip C A B A B C -> (A, B)

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

    !"

  18. 25.

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

    (A) -> Optional<B> = Container<B> B B
  19. 27.
  20. 28.

    What is a context? Container<A> A Container holds 
 the

    instance 
 of type A while adding
 its context instance of type A context
  21. 30.

    • nil / null • failure / error / exception

    • lazy evaluation • unknown number of values • asynchronous operation • … many more!
  22. 31.
  23. 32.
  24. 33.
  25. 34.

    let interestingData = networkClient .fetchData() .flatMap { validateStatusCode($0) } .flatMap

    { deserialize($0) } .map { getSubsetOfData($0) } .reduce { handleSideEffect($0) }
  26. 35.
  27. 39.

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

    .map { ... } .flatMap { ... } multipleValues .map { ... } .flatMap { ... } possibleErrorValue .map { ... } .flatMap { ... }
  28. 40.
  29. 41.
  30. 42.

    As a user, I want to see all my Github

    repos written in Swift language
  31. 45.

    1. Fetch data from Github extension URLSession { func dataTask(

    with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void ) -> URLSessionDataTask }
  32. 46.

    1. Fetch data from Github completionHandler => Future (Data?, URLResponse?,

    Error?) => Result<Data, NetworkError> Future<Result<Data, NetworkError >>
  33. 47.

    2. Deserialize Response is a JSON object in form of

    an array of repos: [ { “name”: “repo_name”, ... }, ... deserialize: (Data) throws -> [Data] throws -> [Repo]
  34. 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 >>
  35. 49.

    3. Filter Swift repos filter: (Repo) -> Bool repos.filter {

    (repo: Repo) -> Bool in repo.language == "Swift" }
  36. 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 { }
  37. 52.
  38. 54.

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

    A Void Optional<A> A ∅ Either<A, Void> A Void
  39. 56.

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

    } } Future< Validated< Validated< Array<Validated<Repo, DeserializationError >>, DeserializationError >, NetworkError > >
  40. 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 >>
  41. 59.

    func sequence<A, E>(_ vs: [Validated<A, E>]) -> Validated<[A], E> Future<

    Validated< Validated< Validated< Array<Repo>, DeserializationError >, DeserializationError >, NetworkError > >
  42. 61.

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

    == Validated<B, E> { ... } } Future< Validated< Validated< Array<Repo>, DeserializationError >, NetworkError >>
  43. 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> >>
  44. 63.

    We’re left with 3 containers-deep nesting // after flattening again

    Future< // 1st level Validated< // 2nd level [Repo], // 3rd level Either<NetworkError, DeserializationError> > >
  45. 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 { }
  46. 65.
  47. 68.

    Inverse of type erasure protocol Protocol { associatedtype A }

    Type erasure struct AnyProtocol<A> 
 : Protocol struct Container<A> “Type rasure” protocol ContainerType { associatedtype A }
  48. 69.

    1. Create protocol protocol ValidatedType { associatedtype PA associatedtype PE

    } extension Validated: ValidatedType { typealias PA = A typealias PE = E }
  49. 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 {}
  50. 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) } } }
  51. 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 } }
  52. 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) } } }
  53. 75.

    !"

  54. 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!
  55. 77.
  56. 78.

    Use code generation Please see this great talk for details:


    Elviro Rocca — Protocol-Oriented Monad Transformers https://www.youtube.com/watch?v=Zmb86zblcto
  57. 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!
  58. 81.