Skip to content

Instantly share code, notes, and snippets.

@broomburgo
Last active February 25, 2019 09:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save broomburgo/bd4871f7c35be3790a7883f829925af3 to your computer and use it in GitHub Desktop.
Save broomburgo/bd4871f7c35be3790a7883f829925af3 to your computer and use it in GitHub Desktop.
A blog answer

Answer to http://disq.us/p/1za4u75.


Hi, your problem is actually quite hard to solve with composition, at least without a profunctor-based approach, which is really hard to do in Swift due to the lack of higher-kinded types.

There is a solution, though, but we must be clear about what we're searching for here. In your ViewState<T> there could be no Prism that points just to T, because the inject function wouldn't know what case to produce with a value of type T: it's probably going to be an Affine.

At the bottom of this answer you'll find all the code needed to implement it: it can be directly copy-pasted into a playground.

To approach this problem let's try to solve a simpler one first: let's assume we only care about the loading and success cases.

We can produce the two interested Prisms:

let prism1 = ViewState.prism.loading
let prism2 = ViewState.prism.loaded
    .then(ViewState.Loaded.prism.success)

Notice that we obtained the second with Prism composition.

To "merge" the Prisms we can zip them, as explained in the article:

let zipped = Prism.zip(
    ViewState.prism.loading,
    ViewState.prism.loaded
        .then(ViewState.Loaded.prism.success))

This will give us a Prism<ViewState<T>, Either<T, T>>, which is not convenient if we only care about T. But it turns out there's a Lens on Either when the 2 type parameters are equal:

extension Either where A == B {
    static var anyLens: Lens<Either, A> {
        return Lens<Either, A>(
            get: {
                switch $0 {
                case let .left(part):
                    return part
                case let .right(part):
                    return part
                }
        },
            set: { part in
                { whole in
                    switch whole {
                    case .left:
                        return .left(part)
                    case .right:
                        return .right(part)
                    }
                }
        })
    }
}

This will basically explore both cases, and get and set the appropriate one, given a particular instance.

We got a Prism<ViewState<T>, Either<T, T>> and a Lens<Either<T, T>, T>: we can easily compose them by turning both into Affine and then composing the affines:

extension ViewState {
    static var loadingSuccessAffine: Affine<ViewState, T> {
        let affine1 = Prism.zip(
            ViewState.prism.loading,
            ViewState.prism.loaded
                .then(ViewState.Loaded.prism.success))
            .toAffine()
        
        let affine2 = Either<T, T>.anyLens.toAffine()

        return affine1.then(affine2)
    }
}

This solves the simpler problem, and gives us a convenient Affine to work with.

But the actual problem also includes the failure(Error, T?), which is different from the others, because it has a tuple of associated types. The fact that T here is actually Optional<T> is not a problem, we can simply use the Prism<Optional, Wrapped> that's also presented in the article. But to focus on the T? we need a Lens that focuses on the rightmost type of a 2-tuple:

func lensSecond<A, B>() -> Lens<(A, B), B> {
    return Lens<(A, B), B>(
        get: { tuple in tuple.1 },
        set: { b in { tuple in (tuple.0, b) }})
}

Because we need to reach the T within, we'll need to compose this Lens with the Prisms for the case and for Optional, and to do that we need to convert everything into an Affine:

let deepAffine = ViewState.prism.loaded
    .then(ViewState.Loaded.prism.failure)
    .toAffine()
    .then(lensSecond().toAffine()
        .then(Optional.prism.toAffine()))

By converting the two previous Prisms into Affines we can try to merge them with some kind of zip function. It turns out that there's a zip for Affine, but it needs a special algebraic data type, that I call Inclusive:

enum Inclusive<A, B> {
    case left(A)
    case center(A, B)
    case right(B)
}

This is like These from Haskell, but I like Inclusive because it models the inclusive-or, that is, A or B or both. For this kind of problem, though, we need a 3-way Inclusive:

enum Inclusive3<A, B, C> {
    case a(A)
    case b(B)
    case c(C)
    case ab(A, B)
    case bc(B, C)
    case ac(A, C)
    case abc(A, B, C)
}

Equipped with this, we can construct our mega-Affine:

extension ViewState {
    static var valueAffine: Affine<ViewState, Inclusive3<T, T, T>> {
        let affine1 = ViewState.prism.loading.toAffine()
        
        let affine2 = ViewState.prism.loaded
            .then(ViewState.Loaded.prism.success)
            .toAffine()
        
        let affine3 = ViewState.prism.loaded
            .then(ViewState.Loaded.prism.failure)
            .toAffine()
            .then(lensSecond().toAffine()
                .then(Optional.prism.toAffine()))
        
        return Affine.zip3(
            affine1,
            affine2,
            affine3)
    }
}

Unfortunately, we can't do much more that this here: there's no equivalent Lens on Inclusive3 (or even the regular Inclusive) because we need to take into account the cases with more that one associated type. The problem here is that, by turning everything into an Affine to unlock the composition, we "forgot" that we started with an enum, for which the cases are exclusive, which means that to retrieve the exclusiveness we need to operate in an "unsafe" way. We can derive an unsafe Lens on Inclusive3:

extension Inclusive3 where A == B, B == C {
    static var anyLens_unsafe: Lens<Inclusive3, A> {
        return Lens<Inclusive3, A>(
            get: {
                switch $0 {
                case let .a(part):
                    return part
                case let .b(part):
                    return part
                case let .c(part):
                    return part
                default:
                    fatalError("Non exclusive")
                }
        },
            set: { part in
                { whole in
                    switch whole {
                    case .a:
                        return .a(part)
                    case .b:
                        return .b(part)
                    case .c:
                        return .c(part)
                    default:
                        fatalError("Non exclusive")
                    }
                }
        })
    }
}

Finally, we can produce an unsafe version of valueAffine:

extension ViewState {    
    static var valueAffine_unsafe: Affine<ViewState, T> {
        return valueAffine.then(Inclusive3.anyLens_unsafe.toAffine())
    }
}

I'd say that in this particular case we don't care about unsafety, because we're sure that the original data structure (ViewState<T>) has a particular shape, so using this would be like force-unwrapping an Optional that we're sure is not nil, but in general I's advise against this.

I hope my answer was useful ;)

Elviro


enum ViewState<T> {
    case idle
    case loading(T)
    case loaded(Loaded)
    
    enum Loaded {
        case success(T)
        case failure(Error, T?)
    }
}

/// Algebra

public enum Either<A, B> {
    case left(A)
    case right(B)
}

extension Either where A == B {
    static var anyLens: Lens<Either, A> {
        return Lens<Either, A>(
            get: {
                switch $0 {
                case let .left(part):
                    return part
                case let .right(part):
                    return part
                }
        },
            set: { part in
                { whole in
                    switch whole {
                    case .left:
                        return .left(part)
                    case .right:
                        return .right(part)
                    }
                }
        })
    }
}
    
enum Inclusive<A, B> {
    case left(A)
    case center(A, B)
    case right(B)
}

enum Inclusive3<A, B, C> {
    case a(A)
    case b(B)
    case c(C)
    case ab(A, B)
    case bc(B, C)
    case ac(A, C)
    case abc(A, B, C)
}

extension Inclusive3 where A == B, B == C {
    static var anyLens_unsafe: Lens<Inclusive3, A> {
        return Lens<Inclusive3, A>(
            get: {
                switch $0 {
                case let .a(part):
                    return part
                case let .b(part):
                    return part
                case let .c(part):
                    return part
                default:
                    fatalError("Non exclusive")
                }
        },
            set: { part in
                { whole in
                    switch whole {
                    case .a:
                        return .a(part)
                    case .b:
                        return .b(part)
                    case .c:
                        return .c(part)
                    default:
                        fatalError("Non exclusive")
                    }
                }
        })
    }
}

/// Prism

struct Prism<Whole, Part> {
    let tryGet: (Whole) -> Part?
    let inject: (Part) -> Whole
}

extension Prism {
    func tryModify(_ transform: @escaping (Part) -> Part) -> (Whole) -> Whole {
        return { whole in self.tryGet(whole).map { self.inject(transform($0)) } ?? whole }
    }
}

extension Prism {
    func then<Subpart>(_ other: Prism<Part, Subpart>) -> Prism<Whole, Subpart> {
        return Prism<Whole, Subpart>(
            tryGet: { self.tryGet($0).flatMap(other.tryGet) },
            inject: { self.inject(other.inject($0)) })
    }
}

extension Prism {
    static func zip <Part1, Part2> (_ a: Prism<Whole, Part1>, _ b: Prism<Whole, Part2>) -> Prism<Whole, Either<Part1, Part2>> where Part == Either<Part1, Part2> {
        return Prism<Whole, Either<Part1,Part2>>(
            tryGet: { a.tryGet($0).map(Either.left) ?? b.tryGet($0).map(Either.right) },
            inject: {
                switch $0 {
                case let .left(value):
                    return a.inject(value)
                case let .right(value):
                    return b.inject(value)
                }
        })
    }
}

extension Optional {
    static var prism: Prism<Optional, Wrapped> {
        return Prism<Optional, Wrapped>(
            tryGet: { $0 },
            inject: { $0 })
    }
}

extension ViewState.Loaded {
    enum prism {
        static var success: Prism<ViewState.Loaded, T> {
            return Prism<ViewState.Loaded, T>(
                tryGet: {
                    guard case let .success(value) = $0 else { return nil }
                    return value
            },
                inject: ViewState.Loaded.success)
        }

        static var failure: Prism<ViewState.Loaded, (Error, T?)> {
            return Prism<ViewState.Loaded, (Error, T?)>(
                tryGet: {
                    guard case let .failure(values) = $0 else { return nil }
                    return values
            },
                inject: ViewState.Loaded.failure)
        }
    }
}

extension ViewState {
    enum prism {
        static var loading: Prism<ViewState, T> {
            return Prism<ViewState, T>(
                tryGet: {
                    guard case let .loading(value) = $0 else { return nil }
                    return value
            },
                inject: ViewState.loading)
        }
        
        static var loaded: Prism<ViewState, ViewState.Loaded> {
            return Prism<ViewState, ViewState.Loaded>(
                tryGet: {
                    guard case let .loaded(value) = $0 else { return nil }
                    return value
            },
                inject: ViewState.loaded)
        }
    }
}

/// Lens

struct Lens<Whole, Part> {
    let get: (Whole) -> Part
    let set: (Part) -> (Whole) -> Whole
}

func lensSecond<A, B>() -> Lens<(A, B), B> {
    return Lens<(A, B), B>(
        get: { tuple in tuple.1 },
        set: { b in { tuple in (tuple.0, b) }})
}

/// Affine

struct Affine<Whole, Part> {
    let tryGet: (Whole) -> Part?
    let trySet: (Part) -> (Whole) -> Whole?
}

extension Lens {
    func toAffine() -> Affine<Whole ,Part> {
        return Affine<Whole, Part>(
            tryGet: self.get,
            trySet: self.set)
    }
}

extension Prism {
    func toAffine() -> Affine<Whole, Part> {
        return Affine<Whole, Part>(
            tryGet: self.tryGet,
            trySet: { part in self.tryModify { _ in part } })
    }
}

extension Affine {
    func tryModify(_ transform: @escaping (Part) -> Part) -> (Whole) -> Whole? {
        return { whole in
            self.tryGet(whole).map(transform).flatMap { b in self.trySet(b)(whole) }
        }
    }
}

extension Affine {
    func then<Subpart>(_ other: Affine<Part, Subpart>) -> Affine<Whole, Subpart> {
        return Affine<Whole, Subpart>(
            tryGet: { whole in
                self.tryGet(whole).flatMap(other.tryGet)
        },
            trySet: { subpart in
                { whole in
                    self.tryGet(whole)
                        .flatMap { part in
                            other.trySet(subpart)(part)
                        }
                        .flatMap { part in
                            self.trySet(part)(whole)
                    }
                }
        })
    }
}

extension Affine {
    static func zip <Part1, Part2> (_ a: Affine<Whole, Part1>, _ b: Affine<Whole, Part2>) -> Affine<Whole, Inclusive<Part1, Part2>> where Part == Inclusive<Part1, Part2>  {
        return Affine<Whole, Inclusive<Part1, Part2>>(
            tryGet: { whole in
                switch (a.tryGet(whole), b.tryGet(whole)) {
                case let (.some(aPart), .some(bPart)):
                    return .some(.center(aPart, bPart))
                case let (.some(aPart), .none):
                    return .some(.left(aPart))
                case let (.none, .some(bPart)):
                    return .some(.right(bPart))
                case (.none, .none):
                    return .none
                }
        },
            trySet: { inclusive in
                { whole in
                    switch inclusive {
                    case let .left(value):
                        return a.trySet(value)(whole)
                    case let .right(value):
                        return b.trySet(value)(whole)
                    case let .center(aPart, bPart):
                        return Optional(whole)
                            .flatMap(a.trySet(aPart))
                            .flatMap(b.trySet(bPart))
                    }
                }
        })
    }
    
    static func zip3 <Part1, Part2, Part3> (_ a: Affine<Whole, Part1>, _ b: Affine<Whole, Part2>, _ c: Affine<Whole, Part3>) -> Affine<Whole, Inclusive3<Part1, Part2, Part3>> where Part == Inclusive3<Part1, Part2, Part3>  {
        return Affine<Whole, Inclusive3<Part1, Part2, Part3>>(
            tryGet: { whole in
                switch (a.tryGet(whole), b.tryGet(whole), c.tryGet(whole)) {
                case (nil, nil, nil):
                    return nil
                case let (aPart?, nil, nil):
                    return .a(aPart)
                case let (nil, bPart?, nil):
                    return .b(bPart)
                case let (nil, nil, cPart?):
                    return .c(cPart)
                case let (aPart?, bPart?, nil):
                    return .ab(aPart, bPart)
                case let (nil, bPart?, cPart?):
                    return .bc(bPart, cPart)
                case let (aPart?, nil, cPart?):
                    return .ac(aPart, cPart)
                case let (aPart?, bPart?, cPart?):
                    return .abc(aPart, bPart, cPart)
                }
        },
            trySet: { inclusive in
                { whole in
                    switch inclusive {
                    case let .a(aPart):
                        return a.trySet(aPart)(whole)
                    case let .b(bPart):
                        return b.trySet(bPart)(whole)
                    case let .c(cPart):
                        return c.trySet(cPart)(whole)
                    case let .ab(aPart, bPart):
                        return Optional(whole)
                            .flatMap(a.trySet(aPart))
                            .flatMap(b.trySet(bPart))
                    case let .bc(bPart, cPart):
                        return Optional(whole)
                            .flatMap(b.trySet(bPart))
                            .flatMap(c.trySet(cPart))
                    case let .ac(aPart, cPart):
                        return Optional(whole)
                            .flatMap(a.trySet(aPart))
                            .flatMap(c.trySet(cPart))
                    case let .abc(aPart, bPart, cPart):
                        return Optional(whole)
                            .flatMap(a.trySet(aPart))
                            .flatMap(b.trySet(bPart))
                            .flatMap(c.trySet(cPart))
                    }
                }
        })
    }
}

/// solution

extension ViewState {
    static var loadingSuccessAffine: Affine<ViewState, T> {
        let affine1 = Prism.zip(
            ViewState.prism.loading,
            ViewState.prism.loaded
                .then(ViewState.Loaded.prism.success))
            .toAffine()
        
        let affine2 = Either<T, T>.anyLens.toAffine()

        return affine1.then(affine2)
    }
}

extension ViewState {
    static var valueAffine: Affine<ViewState, Inclusive3<T, T, T>> {
        let affine1 = ViewState.prism.loading.toAffine()
        
        let affine2 = ViewState.prism.loaded
            .then(ViewState.Loaded.prism.success)
            .toAffine()
        
        let affine3 = ViewState.prism.loaded
            .then(ViewState.Loaded.prism.failure)
            .toAffine()
            .then(lensSecond().toAffine()
                .then(Optional.prism.toAffine()))
        
        return Affine.zip3(
            affine1,
            affine2,
            affine3)
    }
    
    static var valueAffine_unsafe: Affine<ViewState, T> {
        return valueAffine.then(Inclusive3.anyLens_unsafe.toAffine())
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment