Description
Abstract
In this issue I present a way to automatically "derive" instances of typeclasses for any algebraic data types.
Description
We need a new NaturallyIsomorphic
typeclass (protocol) for higher-kinded types:
/// Indicates that this type constructor is naturally isomorphic to the `F` type constructor.
/// It is a natural isomorphism in the categorical sense only when `F` is a functor.
public protocol NaturallyIsomorphic {
associatedtype IsomorphicTo
static func iso<A>(_ value: Kind<IsomorphicTo, A>) -> Kind<Self, A>
static func iso<A>(_ value: Kind<Self, A>) -> Kind<IsomorphicTo, A>
}
Higher-kinded types conforming to NaturallyIsomorphic
can get instances of typeclasses from the type they are IsomorphicTo
[1]. For example, if a type is NaturallyIsomorphic
to a Functor
then it is also a Functor
.
We then need to realize that any algebraic data type is isomorphic to a canonical algebraic data type formed by nested EitherK
s and PairK
s (PairK
is the obvious product type counterpart of EitherK
[2]).
Then, we can automatically get an instance for any typeclass (that has an instance for EitherK
and PairK
) by only making our type NaturallyIsomorphic
to some nesting of EitherK
s and PairK
s. We'll see an example of this equivalence below.
Sample usage
Here we define a non-trivial algebraic data type ADT
. We define it directly with nested EitherK
s and PairK
s, so the implementation of NaturallyIsomorphic
is trivial.
If we want, we can always define smart constructors or computed properties to hide the inner nesting of EitherK
s and PairK
s.
public final class ForADT {}
public final class ADTPartial<A, B>: Kind2<ForADT, A, B> {}
public typealias ADTOf<A, B, C> = Kind<ADTPartial<A, B>, C>
// C * (A + B * C?) = (C * A) + (C * B * C?)
public final class ADT<A, B, C>: ADTOf<A, B, C> {
typealias IsomorphicTo = ADTPartial<A, B>.IsomorphicTo
typealias Value = Kind<IsomorphicTo, C>
/// Smart constructor
public static func c(_ c: C, andA a: A) -> ADT {
ADT(value:
PairK(
Id(c),
EitherK(.left(Const(a)))
)
)
}
/// Smart constructor
public static func c(_ c: C, b: B, maybeC: Option<C>) -> ADT {
ADT(value:
PairK(
Id(c),
EitherK(.right(
PairK(
Const(b),
maybeC
)
))
)
)
}
internal init(value: Value) {
self.value = value
}
let value: Value
}
public postfix func ^ <A, B, C>(_ v: ADTOf<A, B, C>) -> ADT<A, B, C> {
v as! ADT<A, B, C>
}
extension ADTPartial: NaturallyIsomorphic {
// Kind<IsomorphicTo, C> = C * (A + B * C?)
public typealias IsomorphicTo = PairKPartial<
ForId,
EitherKPartial<
ConstPartial<A>,
PairKPartial<
ConstPartial<B>,
OptionPartial
>
>
>
public static func iso<C>(_ value: Kind<IsomorphicTo, C>) -> ADTOf<A, B, C> {
ADT(value: value)
}
public static func iso<C>(_ value: ADTOf<A, B, C>) -> Kind<IsomorphicTo, C> {
value^.value
}
}
extension ADTPartial: EquatableK where A: Equatable, B: Equatable {}
/// This makes ADT a functor on the type parameter A.
extension ADTPartial: Functor {}
Note how we don't need to provide implementations for EquatableK
and Functor
.
Passing test:
func test() {
let v1: ADT<Bool, String, Int> = .c(1, andA: true)
let v2: ADT<Bool, String, Int> = .c(2, b: "Hello", maybeC: nil)
let v3: ADT<Bool, String, Int> = .c(3, b: "Hello", maybeC: .some(4))
let v1Mapped = v1.map({ $0 + 1 })
let v2Mapped = v2.map({ $0 + 1 })
let v3Mapped = v3.map({ $0 + 1 })
let v1MappedOracle: ADT<Bool, String, Int> = .c(2, andA: true)
let v2MappedOracle: ADT<Bool, String, Int> = .c(3, b: "Hello", maybeC: nil)
let v3MappedOracle: ADT<Bool, String, Int> = .c(4, b: "Hello", maybeC: .some(5))
XCTAssertEqual(v1Mapped, v1MappedOracle)
XCTAssertEqual(v2Mapped, v2MappedOracle)
XCTAssertEqual(v3Mapped, v3MappedOracle)
}
Potential implementation
[1] NaturallyIsomorphic
"inherits" instances of typeclasses from its IsomorphicTo
associated type.
extension NaturallyIsomorphic {
public static func transform<A, B>(_ f: @escaping (Kind<IsomorphicTo, A>) -> Kind<IsomorphicTo, B>)
-> (Kind<Self, A>) -> Kind<Self, B> {
Self.iso >>> f >>> Self.iso
}
/// ... and other overloads of `transform` for functions of different shape such as `(A) -> Kind<Self, B>`,
/// omitted here fore brevity.
}
/// If a type is `NaturallyIsomorphic` to an `EquatableK` type then it is also `EquatableK`.
extension NaturallyIsomorphic where IsomorphicTo: EquatableK {
public static func eq<A>(_ lhs: Kind<Self, A>, _ rhs: Kind<Self, A>) -> Bool where A : Equatable {
Self.iso(lhs) == Self.iso(rhs)
}
}
/// If a type is `NaturallyIsomorphic` to a `Functor` then it is also a `Functor`.
extension NaturallyIsomorphic where IsomorphicTo: Functor {
public static func map<A, B>(
_ fa: Kind<Self, A>,
_ f: @escaping (A) -> B) -> Kind<Self, B> {
transform(f |> flip(IsomorphicTo.map))(fa)
}
}
[2] PairK
public final class ForPairK {}
public final class PairKPartial<F, G>: Kind2<ForPairK, F, G> {}
public typealias PairKOf<F, G, A> = Kind<PairKPartial<F, G>, A>
public final class PairK<F, G, A>: PairKOf<F, G, A> {
public init(_ fa: Kind<F, A>, _ ga: Kind<G, A>) {
self.run = Pair(fa, ga)
}
public init(run: Pair<Kind<F, A>, Kind<G, A>>) {
self.run = run
}
let run: Pair<Kind<F, A>, Kind<G, A>>
var first: Kind<F, A> {
run.first
}
var second: Kind<G, A> {
run.second
}
}
public postfix func ^ <F, G, A>(_ fa: PairKOf<F, G, A>) -> PairK<F, G, A> {
fa as! PairK<F, G, A>
}
extension PairKPartial: EquatableK where F: EquatableK, G: EquatableK {
public static func eq<A>(_ lhs: PairKOf<F, G, A>, _ rhs: PairKOf<F, G, A>) -> Bool where A : Equatable {
lhs^.first == rhs^.first && lhs^.second == rhs^.second
}
}
extension PairKPartial: Invariant where F: Functor, G: Functor {}
extension PairKPartial: Functor where F: Functor, G: Functor {
public static func map<A, B>(_ fa: PairKOf<F, G, A>, _ f: @escaping (A) -> B) -> PairKOf<F, G, B> {
PairK(run: fa^.run.bimap({ F.map($0, f) }, { G.map($0, f) }))
}
}
Modules
Bow
Breaking changes
None, new additions.
Drawbacks
I haven't tested, but I assume the performance is worse than manually written typeclass instances. Despite this, I believe the technique presented here is valuable because:
- It allows us to get typeclass instances for free when we don't care about performance
- When we care about performance, the derived instances can be used as test oracle for the manually written instances.
Proposed roadmap
- Open PR with full
PairK
implementation and tests - Open PR with full
NaturallyIsomorphic
implementation and tests - Write documentation page describing the technique to derive typeclass instances.
Open questions
- Can optics be derived with this technique?