key takeaways for https://www.pointfree.co/

1 Functions

Introduces operator |> (forward application) and >>> (forward composition).

  2 |> incr // == incr(2)
  (incr >>> square)(2) // 9
  • Forward composition is the oposite of haskells . composition operator ((b->c)->(a->b)->(a->c))
  • Use precedencegroups to avoid writing brackets
  • Use operators with care.
  • Tooling suffers from Free functions + operators (no autocomplete)
  • Methods don't compose as well, because they have to be associated with a value all the time
  • Pointfree: never refer to the value you are processing, focus on functions and compositions

2 Side effects

Side effects break composability, eg map $ map f can have different side effects than map (f . f), but the same return value.

Take for example writing logs. We can fix the problem of returning logs alongside the computation ((_ x: Int) -> (Int, [String])). But for this kind of function/operator we need a new compose function that appends the logs from both function executions.

  func >=> <A, B, C>(
    _ f: @escaping (A) -> B?,
    _ g: @escaping (B) -> C?
    ) -> ((A) -> C?) {
  
    return { a in
        // same for lists and optionals:
        f(a).flatMap(g)
    }
  }

In haskell, it has type (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c, forward composition for monadic functions.

We can avoid having inputs from side effects with currying. <> is then defined as function composition for functions that return the input type. (In haskell this is more generally defined on semigroups, namely: (<>) :: Semigroup a => a -> a -> a.

3 Algebraic data types

Composite types can also be called algebraic data types, because we can do algebra with them. E.g. if an enum has cases for type A and B we can construct \(A + B\) values from it, where in this notation, \(A\) and \(B\) denote the number of values that can be constructed in the respective type. For pairs that's \(A * B\). Void has 1 value and Never 0.

3.1 Exercises

  1. Exponential. So for \(A->B\), it has \(B^A\) types. Because we choose a value from set \(B\) \(A\) times.
  2. List<A> = 1 + A * List<A>
  3. Optional<Either<A, B>> = 1 + A + B != (A + 1) + (B + 1) = Either<Optional<A>, Optional<B>>. In the second type there is Left nil and Right nil whereas in the first there is just nil for the absence of values.
  4. Either<Optional<A>, B> = 1+A+B = Optional<Either<A, B>>. They are equivalent.
  5. Using the previous definitions for Pair and Either:
            func * <A, B>(lhs: A.Type, rhs: B.Type) -> Pair<A, B>.Type {
              return Pair<A, B>.self
            }
            
            func + <A, B>(lhs: A.Type, rhs: B.Type) -> Either<A, B>.Type {
              return Either<A, B>.self
            }
    

4 Higher order functions

How can we fit functions that we work with every day into our composition? We can define functions that manipulate functions such as curry and flip (equivalent to haskell).

How do we deal with methods? Every method secretly defines a static function, which is already curried in the way that i'ts signature is always Self -> Arguments -> ReturnType Zero argument functions can be a problem, because you have these empty parathesis flying around. Solution: Zurry (zero argument curry).

  func zurry<A>(_ f: () -> A) -> A {
    return f()
  }

Generic and throws functions are problematic, but we can redefine functions such as map and filter to free, curried functions.

  func map<A, B>(_ f: @escaping (A) -> B) -> ([A]) -> [B] {
    return { $0.map(f) }
  }

What's the point? "When they work, they work really well, saving us a lot of boilerplate. When they don’t work, we’re still using the concepts and building intuitions for them."

4.1 Exercises

  1. 3 argument curry
     func curry<A, B, C, D>(_ f: @escaping (A, B, C) -> D) -> (A) -> (B) -> (C) -> D {
         return { a in { b in { c in f(a, b, c) } } }
     }
  1. With e.g. URL.appendingPathComponent these tricks don't work, because you can not disambiguate the overloads.
     let urlForPath = flip(curry(URL.init(fileURLWithPath:isDirectory:)))(true)
  1. As we know from haskell, it's only right associative.
     A->B->C->D -- (A->(B->(C->D)))
  1. uncurry
     func uncurry<A, B, C>(_ f: @escaping (A) -> (B) -> C) -> (A, B) -> C {
         return { (a, b) in f(a)(b) }
     }
  1. reduce
     func reduce<A,E>(f: @escaping(A,E) -> A) -> ([E]) -> (A) -> A {
         return { l in { i in
                        l.reduce(i, f)
         }}
     }
  1. pseudoEither:
     struct PseudoEither<A, B> {
         let left: A?
         let right: B?
     }
     
     func left<A, B>(a: A) -> PseudoEither<A, B> {
         PseudoEither(left: a, right: nil)
     }
     
     func right<A, B>(b: B)-> PseudoEither<A, B> {
         PseudoEither(left: nil, right: b)
     }
     
     let x: PseudoEither<String, Int> = left(a: "wuff")
     let y: PseudoEither<String, Int> = right(b: 42)
  1. nested map
     let arr = [[1, 2], [3, 4]]
     ((map >>> map) { $0 + 1 }) (arr) // [[2, 3], [4, 5]]

5 Functional setters

If you modify data structures (by returning a copy), you will end up repeating a lot of code for transforming just one value. Solution: Define generic functions for applying a function on a property. This reduces boilerplate especially when types are modified, because you need a new copy everytime the type changes.

pair
  |> first(incr)
  |> first(String.init)
  |> second(zurry(flip(String.uppercased)))
// ("43", "SWIFT")

To compose functions within setters, we need to take care that it is done in the correct order. E.g.

  let nested = ((1, true), "Swift")
  nested
  |> (second >>> first) { !$0 }
// ((1, false), "Swift")

Setter composition composes backwards. Setters lift a transformation on an individual value into a function operating on the surrounding datatype.

5.1 Exercises

  1. optional setters
     func map<A>(f: @escaping ((A)->A)) -> (A?) -> A? {
         { optional in
             optional.flatMap(f)
         }
     }
     struct Dog {
         let age: Int?
         let name: String
     }
     
     func propDogAge(f: @escaping (Int) -> Int) -> (Dog) -> Dog {
         { dog in
             Dog(age: map(f: f)(dog.age), name: dog.name)
         }
     }
     
     let snoopy = Dog(age: 1, name: "snoopy")
     
     snoopy |>
       propDogAge(f: { n in n+1 })
  1. For the Dog struct above:
     func propDogName(f: @escaping (String) -> String) -> (Dog) -> Dog {
         { dog in
             Dog(age: dog.age, name: f(dog.name))
         }
     }

Problems: Setters are a lot of boilerplate, and it doesn't scale well with many properties

     struct Dog {
         let name: String
         let location: Location
     }
     
     struct Location {
         let name: String
     }
     
     func dogLocationName(f: @escaping (String) -> String) -> (Dog) -> Dog {
         { dog in
             Dog(name: f(dog.name), location: Location(name: f(dog.location.name)))
         }
     }
     
     func dogLocation(f: @escaping (Location) -> Location) -> (Dog) -> Dog {
         { dog in
             Dog(name: dog.name, location: f(dog.location))
         }
     }
     
     func locationName(f: @escaping (String) -> String) -> (Location) -> Location {
         { location in
             Location(name: f(location.name))
         }
     }
     
     let snoopy = Dog(name: "snoopy", location: Location(name: "central park"))
     
     snoopy |> dogLocationName { $0 + "!" }
     snoopy |> (locationName >>> dogLocation) { $0 + "!" }
     snoopy |> (dogLocation <<< locationName) { $0 + "!" }
  1. Yes we can, but for each tuple type you have to write these functions again.
  2. and 6.
     func setKey<K, V>(f: @escaping (V?)->V?) -> (K) -> (Dictionary<K, V>) -> Dictionary<K, V> {
     {
         key in
         { dict in 
             var dict = dict
             if dict[key] !=  nil { // 6.
                 dict[key] = f(dict[key])
             }
             return dict
         }
     }
     }
  1. The former is the signature of a setter, with the transformation as first argument. The latter can not be a setter, because it does not allow for the passing of a transformation value. In other words: The first signature receives a transformation function \(A \rightarrow B\) whereas the second receives two values, one of type \(A\) and one of type \(B\).

6 Setters and key paths

Key paths are generic over type they apply to, and the specific value, eg.

KeyPath<User, String>

you can use a keypath with array syntax!

user[keyPath: \User.name]

Key paths are compiler generated (generate setters/getters under the hood)

Use prop to compose functions over key paths

  func prop<Root, Value>(_ kp: WritableKeyPath<Root, Value>)
    -> (@escaping (Value) -> Value)
    -> (Root)
    -> Root
  
  (prop(\User.name)) { $0.uppercased() } // User -> User
  // composing:
  prop(\User.location) <<< prop(\Location.name)
  // ((String) -> String) -> (User) -> User
  // Even though that's equivalent to \User.location.name

prop allows you to write neat date formatters, URLrequest configurations etc. pointfree in one expression (without computed props/stateful funcs)..

6.1 Exercises

  1. Dictionary’s subscript key path.
          ((Value?) -> Value?) -> Dictionary<Key, Value> -> Dictionary<Key, Value>
    

    The difference when piping map is that the optional in the signature vanishes, because we lift the transformation in it's monadic context (the monad being the maybe monad).

  2. Functional setters for sets
            func elem<A>(_ a: (A)) -> (@escaping (Bool) -> Bool) -> (Set<A>) -> Set<A> {
                { f in
                    { set in
                        var set = set
                        if (f(set.contains(a))) {
                            set.insert(a)
                        }
                        return set
                    }
                }
            }
    
  3. Array subscript key path on user Guess i misunderstood the task
            prop(\User.favoriteFoods)({
                if $0.isEmpty {
                    return []
                } else {
                    return [$0.first!.uppercased()] + Array($0.dropFirst())
                }
            })
    

    correctAnswer:

            (prop(\User.favoriteFoods[0].name)) { $0.uppercased() }
    
  4. filtering in place
            (prop(\User.favoriteFoods) <<< filter) // (String -> Bool) -> User -> User
    
  5. We will use Result<Value, Error> from Foundation.
            let result = Result<Int, Error>.success(1)
            func value<V, E>(f: @escaping ((V)->V)) -> (Result<V, E>) -> (Result<V, E>) {
                { res in
                    switch res {
                    case .failure:
                        return res
                    case .success(let v):
                        return Result<V, E>.success(f(v))
                    }
                }
            }
            // Error defined equivalently for failure case.
    
    1. No, there are no key paths for structs (Case paths from pointfree as an alternative)
    2. e.g. for prop, the mapper function returns Void, and we simply need to call it, rather than re-assigning.
               func inoutProp<Root, Value>(_ kp: WritableKeyPath<Root, Value>)
               -> (@escaping (inout Value) -> ())
               -> (Root)
               -> Root {
                   { f in
                       { root in
                           var root = root
                           f(&root[keyPath: kp])
                           return root
                       }
                   }
               }
      

7 Getters and key paths

Use get wrapper to bridge between function and keypath world

func get<Root, Value>(_ kp: KeyPath<Root, Value>) -> (Root) -> Value {
  return { root in
    root[keyPath: kp]
  }
}

this is nice:

users
  .filter(get(\.isStaff) >>> (!))

you can do operators as functions 💡.

Generic getters can also be helpful for sorting, max etc. because there you need a compare function A -> A -> Bool. With their, we can write

users
  .max(by: their(get(\.email), <)) // we can also use Comparable instead of passing the compare fn.

We can also write a wrapper for combining elements in reduce:

episodes.reduce(0, combining(get(\.viewCount), by: +))

We use get a lot, so you might consider using an operator ^ as prefix operator.

7.1 Exercises

  1. Find three more standard library APIs that can be used with our get and ^ helpers:
    • filter a list of items on a boolean property
    • Dictionary<A, B>.init(grouping:, by:), grouping by an int property
    • contains(where:)
    • other maps such as compact or flatMap, mapValues etc.
  2. A getter key-path for zero-argument functions wrapping a property… cant think of others
  3. I am not sure if this is what is meant:
            func getFoodAttr<T>(f: KeyPath<Food, T>) -> (User) -> [T] {
                { user in
                    map(f: get(f))(user.favoriteFoods)
                }
            }
            getFoodAttr(f: \.name)(user) // Tacos, Nachos
    
  4. Key paths support optional chaining
            func getLocationAttr<T>(f: KeyPath<Location, T>) -> (User) -> T? {
                { user in
                    map(f: get(f))(user.location)
                }
            }
            getLocationAttr(f: \.name)(user)
            let user2 = User(
                favoriteFoods: [],
                location: nil,
                name: "Blob"
            )
            get(\User.location?.name)(user2)
    
  5. Like this?
            func pluck<V, E>(r: Result<V, E>) -> V? {
                switch r {
                case .success(let v):
                    return v
                default: return nil
                }
            }
            let res: Result<User, Error> = .success(user)
            map(f: get(\User.name))(pluck(r: res))
    

TODO: i don't really understand what is meant with 6,7,8 exercises.

8 Algebraic Data Types: Exponents

As we learned in the exercises from "Algebraic Data Types", there are \(B^A\) functions of type \(A->B\). Since \(a^b^c = a^(b*c)\), we can derive that C->B->A = (B, C) -> A. Shows that curry and uncurry are elemental operations. Since \(a^1 = a\), we can derive zurry and unzurry.

What happens with \(a^0=1\)? Never -> A = Void We can define absurd in swift!

func to<A>(_ f: (Never) -> A) -> Void {
  return ()
}
func from<A>(_ x: Void) -> (Never) -> A {
  return { never in
    switch never {
    }
  }
}

This can actually be useful, eg. in case of Result<Int, Never>.

Remember that inout A -> Void can be transformed to A->A? This means that you can refactor (A,B) -> A into (inout A, B) -> Void, (A, inout B) -> C to (A, B) -> (C, B) and such.

We can also see a correspondence between throws and Result<A, Error>.

There is also the power law \(a^(b+c) = a^b * a ^ c\), equivalent to Either<B, C> -> A = (B -> A, C -> A)

💡We can use exponent laws to understand which functions can't and can not be simplified.

8.1 Exercises

  1. \(1^a = 1\) Void <- a = Void a -> Void = Void
            func to<A>(a: A) -> (Void) -> Void {
                { void in void }
            }
            
            func from<A>(void: Void) -> (A) -> Void {
                { a in void }
            }
    
  2. Case 1: \(0^a = 0 | a != 0\) a -> Never = Never
            func from<A>(a: A) -> (Never) -> (Never) {
                { never in
                    never
                }
            }
            
            func to<A>(never: Never) -> (A) -> (Never) {
                { a in
                    never
                }
            }
    

    Case 2: \(0^a = undefined | a = 0\)

  3. We will know in the next "Algebraic data types" episode 😌
  4. 2^A = Set<A> A -> Bool = Set<A> So we can interpret a set as a function from A to Bool, that returns true if the element is contained, and false otherwise.
  5.          func intersection<A>(a: @escaping ((A) -> Bool), b: @escaping (A) -> (Bool)) -> (A) -> Bool {
                 { e in
                     return a(e) && b(e)
                 }
             }
            
             func union<A>(a: @escaping ((A) -> Bool), b: @escaping (A) -> (Bool)) -> (A) -> Bool {
                 { e in
                     return a(e) || b(e)
                 }
             }
    
  6. \((1+V)^K\) Because we map from the key space to the value space with one additional element nil.
  7.          func to<A, B, C>(_ f: @escaping (Either<B, C>) -> A) -> ((B) -> A, (C) -> A) {
                 ( { b in f(.left(b)) }
                     ,
                   { c in f(.right(c)) }
                 )
             }
            
             func from<A, B, C>(_ f: ((B) -> A, (C) -> A)) -> (Either<B, C>) -> A {
                 let (bToA, cToA) = f
                 return { bc in
                     switch bc {
                     case .left(let b):
                         return bToA(b)
                     case .right(let c):
                         return cToA(c)
                     }
                 }
             }
    
  8.         func to<A, B, C>(_ f: @escaping (C) -> (A, B)) -> ((C) -> A, (C) -> B) {
                ( { c in f(c).0 }
                    ,
                  { c in f(c).1 }
                )
            }
            
                    func from<A, B, C>(_ f: ((C) -> A, (C) -> B)) -> (C) -> (A, B) {
                let (cToA, cToB) = f
                return { c in
                    (cToA(c), cToB(c))
                }
            }
            
    

9 A tale of two flat maps

flatMap is the mondaic bind. use compactMap to filter nil values.

9.1 Exercises

  1. filtered:
           func filtered<A>(_ lst: [A?]) -> [A] {
               lst.compactMap { x in x }
           }
    
  2.          func left<A, B>(_ either: Either<A, B>) -> A? {
                 switch either {
                 case .left(let l):
                     return l
                 default:
                     return nil
                 }
             }
            
             func right<A, B>(_ either: Either<A, B>) -> B? {
                 switch either {
                 case .right(let r):
                     return r
                 default:
                     return nil
                 }
             }
            
             func partitioned<A, B>(_ either: [Either<A,B>]) -> (left: [A], right: [B]) {
                 ( either.compactMap(left)
                     ,
                   either.compactMap(right)
                 )
             }
            
    
  3.          func partitionMap<A, B, C>(
                 _ optional: Optional<A>,
                 _ f: @escaping ((A) -> Either<B, C>)) -> (Optional<B>, Optional<C>) {
                 ( optional.flatMap(f >>> left)
                     ,
                   optional.flatMap(f >>> right)
                 )
             }
    
  4.          func filterMapValues<K, V, R>(_ d: Dictionary<K, V>) -> ((V) -> R?) -> [R] { { f in
                 d.values.compactMap(f)
             }
             }
    
  5.          func partitionMapValues<K, V, A, B>(_ d: Dictionary<K, V>) -> ((V) -> Either<A, B>) -> (lefts: [A], rights: [B]) {
                 { f in
                     Array(d.values).partitionMap(f)
                 }
             }
    
  6.          func optionalEither<A, B>(_ f: @escaping ((A) -> B?)) -> (A) -> Either<B, Void> {
                 { a in
                     if let r = f(a) {
                         return .left(r)
                     } else {
                         return .right(())
                     }
                 }
             }
             
             func filterMap<A, B>(_ f: @escaping ((A) -> B?), _ a: [A]) -> [B] {
                 a.partitionMap(optionalEither(f)).lefts
             }
             
             func predicateOptional<A>(_ f: @escaping (A) -> Bool) -> (A) -> A? {
                 { a in
                     f(a) ? a : nil
                 }
             }
             
             func filter<A>(_ f: @escaping ((A) -> Bool), _ a: [A]) -> [A] {
                 a.partitionMap(optionalEither(predicateOptional(f))).lefts
             }
    
  7. Depends on what is allowed. You'd have to pass it two transformation functions for the left and the right case, that have the same Either<A,B> return type. Then you can partition into (Optional<A>, Optional<B>).

10 Composition without operators

If operators are not the way to go, you can use named functions. The problem is that you need an overload for any number of arguments. This might change in the future for variadic generics.

10.1 Exercises

  1.           func concat<A: AnyObject>(
                  _ fs: ((inout A) -> Void)...
              )
              -> (inout A) -> Void {
                  { a in fs.forEach { $0(&a) } }
              }
    
  2.         func concat<A: AnyObject>(
                _ fs: ((A) -> A)...
            )
            -> (A) -> A {
                { x in
                    return reduce(f: { a, f in f(a) })(fs)(x)
                }
            }
    
  3.        func compose<A, B, C>(_ f: @escaping (B)->C, _ g: @escaping(A) -> B) -> ((A) -> C) {
               { a in f(g(a)) }
           }
           
           struct Dog {
               var favoriteFood: DogFood
           }
           
           struct DogFood {
               var name: String
               var scrumptiousness: Int
           }
           
           let doggieBoy: Dog = compose(
             prop(\Dog.favoriteFood),
              prop(\DogFood.scrumptiousness))({$0+1})(
             Dog(favoriteFood: DogFood(name: "nuggers", scrumptiousness: 1999)
           ))
    

11 TODO Tagged

We can use `Decodable` and `Encodable` to and from json. Sometimes it can be desirable to wrap your datatypes with a custom type, e.g. use an Email struct instead of just String. This can help avoid errors, because the compiler will complain if you pass other fields to e.g. a sendEmail function.

Problem: If you have a custom struct, you need to adapt the json structure. Or implement your custom Decodable instance. Or use RawRepresentable which saves a lot of code. You will have to tag on an Equatable though:

struct Subscription: Decodable {
  struct Id: Decodable, RawRepresentable, Equatable { let rawValue: Int }

  let id: Id
  let ownerId: Int
}

This is really need, but you're gonna repeat the three protocols all over again. Akin to newtype in haskell we can use Tagged<Tag, Type>:

struct Subscription: Decodable {
  typealias Id = Tagged<Subscription, Int>

  let id: Id
  let ownerId: User.Id
}

Tagged uses Conditional conformance to implement Equatable and Decodable:

extension Tagged: Equatable where RawValue: Equatable {
  static func == (lhs: Tagged, rhs: Tagged) -> Bool {
    return lhs.rawValue == rhs.rawValue
  }
}

If you have to instantiate Tagged fields yourself, it makes sense to use ExpressibleBy...Literal protocols.

11.1 Exercises

  1. Expressible by string:
            extension Tagged: ExpressibleByUnicodeScalarLiteral where RawValue: ExpressibleByStringLiteral {
                typealias UnicodeScalarLiteralType = String
            }
            
            extension Tagged: ExpressibleByExtendedGraphemeClusterLiteral where RawValue: ExpressibleByStringLiteral {
                init(extendedGraphemeClusterLiteral value: String) {
                    self.init(stringLiteral: value as! RawValue.StringLiteralType)
                }
            }
            
            extension Tagged: ExpressibleByStringLiteral where RawValue: ExpressibleByStringLiteral {
                init(stringLiteral value: RawValue.StringLiteralType) {
                    self.init(rawValue: RawValue(stringLiteral: value))
                }
            }
    
  2.          extension Tagged: Comparable where RawValue: Comparable {
                 static func < (lhs: Tagged<Tag, RawValue>, rhs: Tagged<Tag, RawValue>) -> Bool {
                     return lhs.rawValue < rhs.rawValue
                 }
             }
             // from swift-overture:
             public func their<Root, Value: Comparable>(
                 _ getter: @escaping (Root) -> Value
             )
             -> (Root, Root) -> Bool {
                 return their(getter, <)
             }
            
            
             users.sorted(by: their(\.id))
    
  3. hmmmmm
             enum AgeTag {}
             typealias Age = Tagged<AgeTag, Int>
    

12 TODO Dependency Injection made easy

Protocol oriented programming for DI is the most common way in swift. They come with a lot of boilerplate. Another solution is to use staticly defined environments that you can swap out at test time:

struct Environment {
  var analytics = Analytics()
  var date: () -> Date = Date.init
  var gitHub = GitHub()
}

var Current = Environment()

The test setup then looks somewhat like this:

  // Define mock dependencies statically
  extension GitHub {
    let static mock = GitHub(fetchRepos: { callback in
      callback(.success([
        GitHub.Repo(
          archived: false,
          description: "Blob's blog",
          htmlUrl: URL("https://www.pointfree.co",
          name: "Bloblog",
          pushedAt: Date(timeIntervalSinceReferenceData: 547152021)
        )
      ])
    })
  }
  
  extension Environment {
      static let mock = Environment(
        analytics: .mock,
        date: { Date(timeIntervalSinceReferenceDate: 557152051) },
        gitHub: .mock
      )
  }
Current = .mock
// Do tests

Unfortunately no example of some clean test suite is provided. Downsides: it will keep you from parallelizing tests, and won't force you to overwrite dependencies with mocks during tests.

12.1 TODO Exercises

13 Dependency Injection Made Comfortable

Nothing much new other than how to easily build mock structures with overture

  extension Array where Element == GitHub.Repo {
    static let mock = [
      GitHub.Repo.mock,
      with(.mock, concat(
        set(\.name, "Nomadic Blob"),
        set(\.description, "Where in the world is Blob?"),
        set(\GitHub.Repo.pushedAt, .mock - 60*60*24*2)
      ))
    ]
    static func mocks(_ count: Int) -> Array {
    return (1...count).map { n in
      with(.mock, concat(
        over(\.name) { "#\(n): \($0)" },
        set(\GitHub.Repo.pushedAt, .mock - 60*60*24*TimeInterval(n))
      ))
    }
  }
  }

13.1 Exercises

No exercises

14 Playground Driven Development

When developing in UIKit, you can build something similar to previews with playgrounds. This requires you to modularize your app, or put the whole app in one framework/package.

import PointFreeFramework
import PlaygroundSupport

let vc = EpisodeListViewController()
PlaygroundPage.current.liveView = vc

15 Dependency Injection Made Composable

16 A Tour of the Composable Architecture: Part 1

  • Cohesive package to solve some problems that Swift UI has
  • Opinionated
  • Focus on modularization and composability
  • Very restrictive regarding side effects.
  • Assertion helper to test the composable architecture, that forces you to deal with all effects
  • Debugging capabilities (.debug to print all actions)
  • Range of example projects

Why view store?

  • Hide state of sub-views so that the current view is only re-rendered when it's own state changes
  • More flexibility binding different front-ends to the state (platform-specific)

Equatable for duplicate filtering Bindings For a list of states, there is a lot of index juggling in bare-metal CA

17 A Tour of the Composable Architecture: Part 2

We can get rid of index juggling with ForEachStore On the reducer side we need to call forEach that supplies

  • Writable key path to a random access collection of subview-states
  • Case path of an action that embeds an index and action into a subview-action (could write this yourself with embed and extract, but CasePaths supplies the backslash for this)
  • Function to create the environment. Or key path

Strong type level guarantees

ForEachStore can be used with stores that only knows about the collection, and the indexed actions. This is done with scope, that extracts the list out of the state, and embeds an index action into the global actions. (Extract global state into local, and embed local state into global)

Then extracting the subviews into their seperate views, the code becomes very short:

    ForEachStore(
                  self.store.scope(
                      state: \.tiles,
                      action: MyActions.tileAction),
                  content: TileView.init)

combine multiple stores, if the global store also needs to handle some logic, that is unrelated to the list of subviews.

18 A Tour of the Composable Architecture: Part 3

Exhaustive tests with TestStore. Basic workflow:

  1. Create a TestStore
           let store = TestStore(
             initialState: AppState(
               todos: [
                 Todo(
                   description: "Milk",
                   id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
                   isComplete: false
                 )
               ]
             ),
             reducer: appReducer,
             environment: AppEnvironment()
           )
    
  2. Create exhaustive assertions (effects that are not tested will throw an error)
           store.assert(
           .send(.todo(index: 0, action: .checkboxTapped)) {
             $0.todos = [
               Todo(
                 description: "Eggs",
                 id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!,
                 isComplete: false
               ),
               Todo(
                 description: "Milk",
                 id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
                 isComplete: true
               )
             ]
           }
           )
    

Some more wisdom:

  • Inject dependencies via environment
  • Be explicit in the assertions
  • use an inline struct to define your cancellable id's:
          case .todo(index: _, action: .checkboxTapped):
          struct CancelDelayId: Hashable {}
          
          return Effect(value: .todoDelayCompleted)
            .delay(for: 1, scheduler: environment.mainQueue)
            .eraseToEffect()
            .cancellable(id: CancelDelayId(), cancelInFlight: true)
    

19 A Tour of the Composable Architecture: Part 4

How to test async effects? Test helper will complain if some effects are still running.

  • don't use XCTWaiter and the like
  • Inject DispatchQueue using AnySchedulerOf<DispatchQueue> and use TestScheduler in the tests. You can then just advance your scheduler and don't have to wait.

20 Designing Dependenciecs: The problem

This episode introduces the already common approach of abstracting away dependencies via a protocol. It then builds the case for scrapping the protocol. (No exercises)

21 Designing Dependencies: Modularization

Dependencies can now easily split into modules, and we can even seperate implementation and interface if needed. (No exercises)

22 Designing Dependencies: Reachability

Discusses how to modularise and style a wrapper library around a system API (NWPathMonitor). (No exercises)

23 Designing Dependencies: Core Location

Interesting showcase with a more complicated dependency including delegates and NSObject inheritance. Keeping a reference to the delegate after the initializer function exits is done via receiveEvents

var delegate: Delegate? = Delegate(subject: subject)
locationManager.delegate = delegate
return Self(
  delegate: delegate
    .handleEvents(receiveCancel: { delegate = nil }
    .eraseToAnyPublisher()
  ...
)

24 Designing Dependencies: The point

This episode demonstrates how freely you can now play with your dependencies. With this approach you can

  • write tests with strong guarantees, e.g. that certain

API's are not called.

  • Create transformations/setters on dependencies
  • Create decorations on dependencies, such as "every effect is delayed by 1 second"
  • Create a mix of dependencies, such as a live core location client that only has the final location result hardcoded
  • Write a debug screen that arbitrarily taps into your environment

25 Redacted SwiftUI: The problem

The problem is that it's hard to seperate concerns in vanilla swiftui. When you redact a view you don't want any interaction to happen, hence you basically want to swap out your business logic with no-ops. In a traditional swiftui setup, you'd have to sprinkle your code all over with early exits to achieve that.

26 Redacted SwiftUI: The Composable Architecture

The idea is simple: In CA, Just swap in a Store that has some placeholder data, no reducer and no environment while loading. You can use the .empty extension of Reducer for a reducer that doesn't care about any actions. And a void env because that's not gonna be used anyway.

27 The Point of Redacted SwiftUI: Part 1

  • If you have a subview that you unredact within your bigger view,

you can selectively add back some logic into your empty reducer

  • You cann call out to another reducer like this
appReducer.run(&state, &action, &environment)

27.1 Exercises

  1. My solution:
    func applying<T: View, I: View>(view: I, @ViewBuilder block: (I) -> T) -> T {
        block(view)
    }

The reference solution uses an extension:

extension View {
  func applying<V: View>(
    @ViewBuilder _ builder: @escaping (Self) -> V
  ) -> some View {
    builder(self)
  }
}

2, 3 & 4: For this we need to migrate the onboarding view to the composable architecture as well. we can then react on actions happening in the "subview" (the app):

     case .placeholderAction(.todo(let id, action: .checkBoxToggled)):
         guard state.step == .todos else { return .none }
         state.placeholderState.todos[id: id]?.isComplete.toggle()
         return .none

28 The Point of Redacted SwiftUI: Part 2

This shows the reference solutions of part 1: You can do it more elegant with case let where, and run the original app reducer:

case let .placeholderAction(action) where state.step == .todos:
    switch action {
    case .todo(id: _, action: .checkBoxToggled),
         .sortCompletedTodos:
        return appReducer
          .run(&state.placeholderState, action, environment)
          .map(OnboardingAction.placeholderAction)

29 Concise Forms: SwiftUI

  • SwiftUI Form arrangement is a great way to build simple forms
  • Split sections with Section
  • State handling and side effects quickly become messy with vanilla SwiftUI (guess who saves the day, the composable architecture of course)

30 Concise Forms: Composable Architecture

This episode mainly showcases basic TCA stuff, and how it makes state mutations in forms more readable and testable, albeit being a bit more verbose.

30.1 Exercises

  1. Simple test, just a state change. We can also make sure to error in any dependencies now unused (registerPush for example).
store.assert(
            .send(.notificationSettingsResponse(UserNotificationsClient.Settings.init(authorizationStatus: .denied))) {
                $0.sendNotifications = false
                $0.alert = .init(title: "You need to enable permissions from iOS settings")
            }
        )

(The reference solution tested a different user flow rather than the individual Action of a denied response).

store.assert(
            .send(.sendNotificationsChanged(true)),
            .receive(.notificationSettingsResponse(.init(authorizationStatus: .denied))) {
                $0.sendNotifications = false
                $0.alert = .init(title: "You need to enable permissions from iOS settings")
            }
        )

Reference tests dismissing the alert as well.

31 Concise Forms: Bye Bye Boilerplate

Forms can be incredibly simpler than the "naive" TCA approach.

  1. Wrap form actions (getters and setters of key paths) in a generic struct
struct FormAction<Root>: Equatable {
  let keyPath: PartialKeyPath<Root>
  let value: AnyEquatable
  let setter: (inout Root) -> Void

  init<Value>(
    _ keyPath: WritableKeyPath<Root, Value>,
    _ value: Value
  ) where Value: Equatable {
    self.keyPath = keyPath
    self.value = AnyEquatable(value)
    self.setter = { $0[keyPath: keyPath] = value }
  }

  // Note this is just an alias for init, for better readability
  static func set<Value>(
    _ keyPath: WritableKeyPath<Root, Value>,
    _ value: Value
  ) -> Self where Value: Equatable {
    self.init(keyPath, value)
  }

  static func == (lhs: FormAction<Root>, rhs: FormAction<Root>) -> Bool {
    lhs.keyPath == rhs.keyPath && lhs.value == rhs.value
  }
}
  1. Use a single action for all state changes by form interactions:
case form(FormAction<SettingsState>)
  1. use viewStore.binding with key store to hook the action into the form (⚠ This method seems deprecated, more info in Safer, Conciser Forms: Part 1&2)
extension ViewStore {
  func binding<Value>(
    keyPath: WritableKeyPath<State, Value>,
    send action: @escaping (FormAction<State>) -> Action
  ) -> Binding<Value> where Value: Hashable {
    self.binding(
      get: { $0[keyPath: keyPath] },
      send: { action(.init(keyPath, $0)) }
    )
  }
}

// And then:
Picker(
              "Top posts digest",
              selection: viewStore.binding(
                keyPath: \.digest,
                send: ConciseSettingsAction.form
              )
...
  1. Reduce even more boiler plate by allowing switches on the form action on first level:
func ~= <Root, Value> (
  keyPath: WritableKeyPath<Root, Value>,
  formAction: FormAction<Root>
) -> Bool {
  formAction.keyPath == keyPath
}

neat!

31.1 Exercises:

  1. AnyEquatable
struct AnyEquatable: Equatable {
    let value: Any
    let equals: ((Any) -> Bool)!
    init<E>(_ base: E) where E : Equatable {
        self.value = base
        self.equals = { $0 as? E == base }
    }
}

func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
    lhs.equals(rhs.value)
}

(almost equal to reference solution).

32 Concise Forms: The Point

This presents concise forms in action within isowords. ⚠ In the current version of TCA, the action and higher level reducer have been renamed more generally to BindingAction<T> and .binding().

33 SwiftUI Animation: The Basics

Implicit animations (.animation(_,value:)) vs explicit animations (withAnimation {}). Implicit animations can give great results with very little work but are unsuitable for fine-grained control, because they animate everything in the view hirarchy. And the semantics of overriding animations is not clear. E.g. if/else branches rather than ternary operator can mess up your animation semantics.

withAnimation is a special case of a continuation: (A -> R) -> R (where A is Void in this case). Animate bindings with binding.animation() instead of withAnimation.

33.1 Exercises

  1. (Might be an ugly solution, don't know the best practices) We can create the work on the dispatch queue as WorkItem's and cancel them later.
@State private var animationStack: [DispatchWorkItem] = []
// ...

   Button("Cycle colors") {
        [Color.red, .blue, .green, .purple, .black]
          .enumerated()
          .forEach { offset, color in
              let work = DispatchWorkItem(block: {
                  withAnimation(.linear) {
                      self.viewModel.circleColor = color
                  }
              })
              self.animationStack.append(work)
              DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(offset), execute: work)
          }
 // ...
      Button("Reset") {
          for work in self.animationStack {
              work.cancel()
          }
          animationStack.removeAll()
          // ...
      }
  1. Easy!
class ViewModel: ObservableObject {
    @Published var circleCenter = CGPoint.zero
    @Published var circleColor = Color.black
    @Published var isCircleScaled = false
    @Published var isResetting = false
}
...

struct ContentView: View {
    @StateObject var viewModel: ViewModel
    /// Then change all usages of self.someState to self.viewModel.someState
}

34 Better Test Dependencies: Exhaustivity

  • Make those functions/dependencies fail that are not used in the test,

e.g. by providing an unimplemented helper. For example

extension UUID {
  static let unimplemented: () -> UUID = { fatalError() }
}

This will assert which dependencies are not used, strengthening your tests. Also if you add more usage of the dependencies later, the tests will tell you exactly where to update.

  • Record function arguments by passing closures like so:
extension AnalyticsClient {
  static func test(onEvent: @escaping (Event) -> Void) -> Self {
    Self(
      track: { event in
        .fireAndForget {
          onEvent(event)
        }
      }
    )
  }
}

34.1 Exercises

  1. An assertion helper that asserts the events and removes all of them
  func assertTracked(events: inout [AnalyticsClient.Event]) {
    XCTAssertEqual(events, self.events)
    events.removeAll()
  }

(But why not re-initialise the captured data every time?)

35 Better Test Dependencies: Failability

  • use XCTDynamicTestOverlay
  • create a failing dependency "constructor'
static let failing = Self(
        execute: { language in
            XCTFail("MyService.execute(\(language)) not implemented")
            return SomeDummyData()
        }
    )

it will not crash the testsuite. CA can also better point out where the error is. There is a neat failing `Effect` in CA: Effect.failing(prefix: String)

36 A Tour of Isowords: Part 1

Describes the hyper-modularization, domain modelling and testing strategy for isowords.

37 A Tour of Isowords: Part 2

Demonstrates how the composable architecture can be used to abstract away seperate features like app clips and app store videos on top of the existing game logic, without littering it with additional code.

38 Async Refreshable: SwiftUI

You can use .refreshable { <async closure> } to implement a simple pull to refresh. We could use for example the new async URLSession to perform the data refresh, since refreshable gives us an async context.

let (data, _) = try await URLSession.shared.data(...)

If we want to be able to cancle such asnychronous work, it needs to be wrapped in a task:

let task = Task {
  //...perform work
}
task.cancel()

If this task is part of your ObservableObject view model and you want to display state based on weather the task is running or not, you had to mark it as @Published, so that SwiftUI can recompute the view when the State of Task execution changes.

When performing async things that then modify some state on completion, it can make sense to use @MainAction method annotation, to make sure work is resumed on the main thread, after finishing the async workload.

The episode then goes to explain that e.g. displaying loading state and cancelling requests when doing async tasks is hard to test. For example, it seems you have to work with TaskSleep and artificial delays to hook into the state of the view model where the request is currently executing.

39 Async Refreshable: ComposableArchitecture

When the refreshable closure is called, the loading spinner is shown as long as an async task is running in the asynchronous context of it. This is a problem with the composable architecture: While you of course can make changes to the store from an async context, the call to make these changes returns immediately.

Solution: Use a boolean to reflect weather an async process is running

func send(
  _ action: Action,
  `while` isInFlight: @escaping (State) -> Bool
) async {
    self.send(action)
    await withUnsafeContinuation { continuation in
        var cancellable: Cancellable?
        cancellable = self.publisher
          .filter { !isInFlight($0) }
          .prefix(1)
          .sink { _ in
              continuation.resume(returning: ())
              _ = cancellable
          }
    }
}

39.1 Exercises

  1. My solution, probably with some race conditions:
extension ViewStore {
  func send(
    _ action: Action,
    while predicate: @escaping (State) -> Bool
  ) async {
    self.send(action)
      var cancellable: Cancellable?
      let onCancel = { cancellable = nil }
      await withTaskCancellationHandler(operation: {
          await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
              cancellable = self.publisher
                  .filter { !predicate($0) }
                  .prefix(1)
                  .sink { _ in
                      continuation.resume()
                      _ = cancellable
                  }
          }
      }, onCancel: { onCancel() })
  }
}

Reference solution:

extension ViewStore {
  func send(
    _ action: Action,
    `while` isInFlight: @escaping (State) -> Bool
  ) async {
    self.send(action)

    var cancellable: Cancellable?
    try? await withTaskCancellationHandler(
      handler: { [cancellable] in cancellable?.cancel() },
      operation: {
        try Task.checkCancellation()
        try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Void, Error>) in
          guard !Task.isCancelled else {
            continuation.resume(throwing: CancellationError())
            return
          }
          cancellable = self.publisher
            .filter { !predicate($0) }
            .prefix(1)
            .sink { _ in
              continuation.resume()
              _ = cancellable
            }
        }
      }
    )
  }
}

(In my solution i don't check for cancellation) Also i'm not sure how the capture list works. Won't it capture always nil, so the cancelable will actually never be cancelled?

  1. I don't think we can write it in terms of the previous function, because we cannot await in withAnimation block.

My monke brain solution would be to copy the previous function and wrap the store.send inside a withAnimation(animation){}. Reference solution: Extract the code after store.send and use it in both functions.

  1. From what we learned so far, this should be quite easy:
extension ViewStore {
    var stream: AsyncStream<State> {
        return AsyncStream { continuation in
            var cancellable: Cancellable?
            cancellable = self.publisher
                .sink { newState in
                    continuation.yield(newState)
                }
            continuation.onTermination = {  @Sendable [cancellable] _ in
                cancellable?.cancel()
            }
        }
    }
}

The reference solution doesn't mind about the onTermination, and handles completions by calling continuation.finish().

  1. This is much shorter now with stream:
extension ViewStore {
    func suspend(while predicate: @escaping (State) -> Bool) async {
        let stream = self.stream
        .filter { !predicate($0) }
        .prefix(1)
        for try await _ in stream { }
    }
}

It can be more elegant without the weird for loop:

extension ViewStore {
  func suspend(while predicate: @escaping (State) -> Bool) async {
    _ = await self.stream
      .filter { !predicate($0) }
      .first(where: { _ in true })
  }
}

40 Safer, Conciser Forms: Part 1

  • use @BindableState in your TCA state property to opt-in to make it bindable
  • This makes the domain logic safer because the view cannot arbitrarily mutate the state
  • use case binding(BindingAction<SpellingChallengeState>) to handle binding actions

some swift learnings:

  • Implement projectedValue to expose the wrapping type when writing your own property wrapper.

Then access the wrapping type with a $ prefix. By default, the wrapping type is just exposed privately and accessed by a _ prefix.

  • Implement getter and setters within projectedValue to derive writable key paths.

41 Safer, Conciser Forms: Part 2

We can omit stating the binding action case when we derive bindings from bindable state, or when we chain the higher level .binding(Action) reducer. Swift enums can conform to protocols with cases, like so:

protocol BindableAction {
  associatedtype State
  static func binding(_: BindingAction<State>) -> Self
}

You could even simplify creating bindings by implementing a dynamic member lookup for the view store. This has been deprecated in TCA though, because it causes some problems in the reducer. If we did't care about the type safety (Only Bindable states can be bound to) we theoretically could use this new syntax that allows bindings to be passed to closures:

struct MyView: View {
  @State var array = [1, 2, 3]
  var body: some View {
    ForEach(self.$array, id: \.self) { $number in
      let _ = $number as Binding<Int>
      let _ = number as Int
    }
  }
}