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 Prism
s:
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 Prism
s 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 Prism
s 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 Prism
s into Affine
s 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())
}
}