Skip to content

["Request"] Simulation for derive (generic programming) #643

Open
@ferranpujolcamins

Description

@ferranpujolcamins

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 EitherKs and PairKs (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 EitherKs and PairKs. 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 EitherKs and PairKs, 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 EitherKs and PairKs.

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

  1. Open PR with full PairK implementation and tests
  2. Open PR with full NaturallyIsomorphic implementation and tests
  3. Write documentation page describing the technique to derive typeclass instances.

Open questions

  • Can optics be derived with this technique?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions