项目作者: GuiBrandt

项目描述 :
Some experiments with annotation processors, code generation, higher kinded types (sort of) and typeclasses (sort of) in Kotlin
高级语言: Kotlin
项目地址: git://github.com/GuiBrandt/higher-kt.git
创建时间: 2021-08-19T14:18:54Z
项目社区:https://github.com/GuiBrandt/higher-kt

开源协议:

下载


Higher Kotlin

Some experiments with annotation processors, code generation, higher kinded types
(sort of) and typeclasses (sort of) in Kotlin. Most of the stuff here isn’t practical.

Higher-Kinded types

See: https://en.wikipedia.org/wiki/Kind_(type_theory)

Implemented as suggested by Yallop & White (2014) in their paper on higher-kinded
polymorphism for languages that do not support it.

Usage

Just annotate some class with @Higher and have the annotation processor on scope
(see “Using kapt”):

  1. @Higher
  2. data class Pair<A, B>(val left: A, val right: B) { companion object }

(P.S.: this requires a companion object on the target class)

Interface

The annotation processor generates the following entities:

  • {Class}Kind (e.g. PairKind), an empty object whose sole purpose is serving
    as a tag for higher-kind type expressions (more on those later)
  • Two projection/injection functions to transition between the “lower-kinded”
    and “higher-kinded” worlds:
    • {Class}.Companion.inj (e.g. Pair.inj). An injection function mapping an
      object (e.g. Pair<A, B>) to its “higher-kinded” equivalent (e.g. Ap2<PairKind, A, B>).
      This is only necessary because there’s no way to modify the class to implement
      the corresponding interface with an annotation processor, unfortunately.
    • {Class}.Companion.prj (e.g. Pair.prj). The inverse function of inj.
  • Two helper functions for lifting/unlifting functions to/from the “higher-kinded” world:
    • {Class}.Companion.lift takes {Class}<A, B, ...> -> {Class}<C, D, ...> to
      ApN<{Class}Kind, A, B, ...> -> Ap{N}<{Class}Kind, C, D, ...>
    • {Class}.Companion.lift is the inverse function of lift.

Higher-Kind type expressions

We define the following (purposefully empty) interface:

  1. interface Ap<T, X>

A higher-kinded type expression is any valid parametrization of Ap.

Kinds are curried, which means a kind of two arguments (* -> * -> *) is just a
nested parametrization of Ap: Ap<Ap<Kind, A>, B>.
We provide a shorthand in the form of Ap{N}<Kind, A, B, ...> = Ap<Ap<Ap<...>, A>, B>, up
to 8 arguments (i.e. Ap2, Ap3, … Ap8).

Example with lists

For instance, suppose the following type:

  1. sealed class List<T> { companion object }
  2. class Nil<T> : List<T>()
  3. data class Cons<T>(val head: T, val tail: List<T>) : List<T>()

We could refer to the “higher kind” of List by creating a tag (i.e. an empty object) for it:

  1. object ListKind // List<T> ~ Ap<ListKind, T>

Obviously, there’s nothing linking List to ListKind. We do this by defining injection and
projection functions from List<T> to Ap<ListKind, T> and vice-versa, respectively:

  1. private data class ListWrapper<T>(val it: List<T>): Ap<ListKind, T>
  2. fun <T> inject(list: List<T>): Ap<ListKind, T> = ListWrapper(list)
  3. // Assume an unique implementation of Ap<ListKind, T>
  4. fun <T> project(it: Ap<ListKind, T>): List<T> = (it as ListWrapper<T>).it

Or simply:

  1. object ListKind
  2. sealed class List<T>: Ap<ListKind, T> {
  3. companion object {
  4. fun <T> inject(list: List<T>): Ap<ListKind, T> = list
  5. fun <T> project(it: Ap<ListKind, T>): List<T> = it as List<T>
  6. }
  7. }
  8. class Nil<T> : List<T>()
  9. data class Cons<T>(val head: T, val tail: List<T>) : List<T>()

The first approach is essentially what the annotation processor does, since it can’t
change List to make it implement Ap<ListKind, T>.

References

Typeclasses

One interesting application for higher-kinded types is in typeclasses, like
those seen in Haskell.

Sadly, those too require some support from the language. We can work our way around
this, though in a similar way to what is done in Scala Cats: a typeclass
signature is replaced by an interface, and an instance is replaced by an object that
implements the interface.

We still cannot automatically derive instances from a set of constraints
(e.g. (Monoid a, Monoid b) => Monoid (a, b)), but it’s simple enough (although significantly
more verbose) to simulate this with functions
(e.g. Pair<A, B>.monoid: (Monoid<A>, Monoid<B>) -> Monoid<Pair<A, B>>).

Example with Functor

See https://en.wikipedia.org/wiki/Functor_(functional_programming)

It’s pretty straightforward to define the interface for a Functor:

  1. interface Functor<F> {
  2. fun fmap(f: (A) -> B): (Ap<F, A>) -> Ap<F, B>
  3. }

Notice that we couldn’t possibly have defined it without Ap, though, because F<A>
would be syntactically invalid in Kotlin.

Now let’s use the List kind (this time with all the functions generated by the
annotation processor, instead of done by hand) to define a functor for lists:

  1. @Higher
  2. sealed class List<T> { companion object }
  3. class Nil<T> : List<T>()
  4. data class Cons<T>(val head: T, val tail: List<T>) : List<T>()
  5. fun List.Companion.functor() = object : Functor<ListKind> {
  6. override fun <X, Y> fmap(f: (X) -> Y): (Ap<ListKind, X>) -> Ap<ListKind, Y> =
  7. List.lift { list: List<X> ->
  8. when (list) {
  9. is Nil -> Nil()
  10. is Cons -> Cons(f(list.head), List.unlift(fmap(f))(list.tail))
  11. }
  12. }
  13. }

Using the instance of functor then is not too hard:

  1. fun main() {
  2. val list = Cons(1, Cons(2, Cons(3, Nil())))
  3. val double = { x: Int -> x * 2 }
  4. val doubleList = List.unlift(List.functor().fmap(double))
  5. println(doubleList(list)) // Cons(head=2, tail=Cons(head=4, tail=Cons(head=6, tail=io.github.higherkt.sample.Nil@4769b07b)))
  6. }

We could also simplify the declaration of doubleList by defining a shorthand
for the application of unlift to an fmap‘ed function, or even hide the functor
altogether with an instance method (a.k.a. map):

  1. fun <X, Y> List.Companion.fmap(f: (X) -> Y): (List<X>) -> List<Y> = List.unlift(List.functor().fmap(f))
  2. fun <X, Y> List<X>.map(f: (X) -> Y): List<Y> = List.fmap(f)(this)

This shows that the need to convert between Ap and List is indeed inconvenient,
but can usually be worked around fairly easily.

[WIP]

TODO: Polynomial functors, cata/ana/hylo/paramorphisms.