https://github.com/guibrandt/higher-kt
Some experiments with annotation processors, code generation, higher kinded types (sort of) and typeclasses (sort of) in Kotlin
https://github.com/guibrandt/higher-kt
annotation-processor category-theory code-generation functional-programming higher-kinded-types kotlin typeclasses
Last synced: about 1 year ago
JSON representation
Some experiments with annotation processors, code generation, higher kinded types (sort of) and typeclasses (sort of) in Kotlin
- Host: GitHub
- URL: https://github.com/guibrandt/higher-kt
- Owner: GuiBrandt
- Created: 2021-08-19T14:18:54.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2021-08-19T16:26:25.000Z (over 4 years ago)
- Last Synced: 2024-11-12T09:50:37.761Z (over 1 year ago)
- Topics: annotation-processor, category-theory, code-generation, functional-programming, higher-kinded-types, kotlin, typeclasses
- Language: Kotlin
- Homepage:
- Size: 73.2 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# 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"][kapt]):
```kotlin
@Higher
data class Pair(val left: A, val right: B) { companion object }
```
(P.S.: this requires a companion object on the target class)
[kapt]: https://kotlinlang.org/docs/kapt.html
#### 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`) to its "higher-kinded" equivalent (e.g. `Ap2`).
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} -> {Class}` 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:
```kotlin
interface Ap
```
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, B>`.
We provide a shorthand in the form of `Ap{N} = Ap, A>, B>`, up
to 8 arguments (i.e. `Ap2`, `Ap3`, ... `Ap8`).
##### Example with lists
For instance, suppose the following type:
```kotlin
sealed class List { companion object }
class Nil : List()
data class Cons(val head: T, val tail: List) : List()
```
We could refer to the "higher kind" of List by creating a tag (i.e. an empty object) for it:
```kotlin
object ListKind // List ~ Ap
```
Obviously, there's nothing linking `List` to `ListKind`. We do this by defining injection and
projection functions from `List` to `Ap` and vice-versa, respectively:
```kotlin
private data class ListWrapper(val it: List): Ap
fun inject(list: List): Ap = ListWrapper(list)
// Assume an unique implementation of Ap
fun project(it: Ap): List = (it as ListWrapper).it
```
Or simply:
```kotlin
object ListKind
sealed class List: Ap {
companion object {
fun inject(list: List): Ap = list
fun project(it: Ap): List = it as List
}
}
class Nil : List()
data class Cons(val head: T, val tail: List) : List()
```
The first approach is essentially what the annotation processor does, since it can't
change `List` to make it implement `Ap`.
### References
- Yallop, Jeremy, and Leo White. "Lightweight higher-kinded polymorphism."
International Symposium on Functional and Logic Programming. Springer, Cham, 2014.
Available at https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf.
## Typeclasses
One interesting application for higher-kinded types is in typeclasses, like
[those seen in Haskell][hs-typeclasses].
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][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.monoid: (Monoid, Monoid) -> Monoid>`).
### Example with `Functor`
See https://en.wikipedia.org/wiki/Functor_(functional_programming)
It's pretty straightforward to define the interface for a Functor:
```kotlin
interface Functor {
fun fmap(f: (A) -> B): (Ap) -> Ap
}
```
Notice that we couldn't possibly have defined it without `Ap`, though, because `F`
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:
```kotlin
@Higher
sealed class List { companion object }
class Nil : List()
data class Cons(val head: T, val tail: List) : List()
fun List.Companion.functor() = object : Functor {
override fun fmap(f: (X) -> Y): (Ap) -> Ap =
List.lift { list: List ->
when (list) {
is Nil -> Nil()
is Cons -> Cons(f(list.head), List.unlift(fmap(f))(list.tail))
}
}
}
```
Using the instance of functor then is not too hard:
```kotlin
fun main() {
val list = Cons(1, Cons(2, Cons(3, Nil())))
val double = { x: Int -> x * 2 }
val doubleList = List.unlift(List.functor().fmap(double))
println(doubleList(list)) // Cons(head=2, tail=Cons(head=4, tail=Cons(head=6, tail=io.github.higherkt.sample.Nil@4769b07b)))
}
```
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`):
```kotlin
fun List.Companion.fmap(f: (X) -> Y): (List) -> List = List.unlift(List.functor().fmap(f))
fun List.map(f: (X) -> Y): List = 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.
[hs-typeclasses]: https://www.haskell.org/tutorial/classes.html
[scala-cats]: https://typelevel.org/cats/typeclasses.html
## [WIP]
TODO: Polynomial functors, cata/ana/hylo/paramorphisms.