-
Notifications
You must be signed in to change notification settings - Fork 382
Compile-time Extension Interfaces #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
13df9dd
7494c84
a5f9659
de8032b
47e1348
eb4f687
3777fa6
374e848
2f2f27e
3b568bf
7d10901
f8dce43
323a550
a105496
505619c
115a483
8a7dd15
8a74e37
20976f6
73b5d33
7027bde
4a98c86
0496c73
da913fe
6a10e93
00d6aca
d47eeef
f495563
d70f40c
23b5276
4671389
84d16ce
b632229
2a8b95a
3b74240
5f7b61c
ce47c2e
2417af2
34d89c8
97f772f
c6835ea
9b6c6fd
f4ba65a
ae5c0a7
8c0737a
eff56ad
33f55fc
988c8a9
94a1a99
bcbd746
dfc0b64
a4fd7f4
a32eaa5
c811a73
e6ba5e6
5485db1
ed8561e
535c453
fba1db3
c563312
21aeaed
3dd43f5
c8bf869
7792dd7
67c8f3e
6ba2577
87d1c43
531711d
bd1416d
7bed642
87eafe6
3753301
951ea3d
297e65f
071d6b7
6ac0d48
e564b61
21003c6
83a1a94
abcd547
46e6a34
e863b25
d488d79
78f5459
da6a274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| # Type classes | ||
|
|
||
| * **Type**: Design proposal | ||
| * **Author**: Raul Raja | ||
| * **Status**: New | ||
| * **Prototype**: - | ||
|
|
||
| ## Summary | ||
|
|
||
| The goal of this proposal is to enable `typeclasses` and lightweight `Higher Kinded Types` in Kotlin to enable ad-hoc polymorphism and better extension syntax. | ||
| Type classes is the most important feature that Kotlin lacks in order to support a broader range of FP idioms. | ||
| Kotlin already has an excellent extension mechanism where this proposal fits nicely. As a side effect `Type classes as extensions` also allows for compile time | ||
| dependency injection which will improve the current landscape where trivial applications rely on heavy frameworks based on runtime Dependency Injection. | ||
|
|
||
| ## Motivation | ||
|
|
||
| * Support Typeclass evidence compile time verification | ||
| * Support a broader range of Typed FP patterns | ||
| * Enable multiple extension functions groups for type declarations | ||
| * Enable compile time DI through the use of the Typeclass pattern | ||
|
|
||
| ## Description | ||
|
|
||
| We propose to introduce a new top level declaration `typeclass` that allows for generic definition of typeclasses and their instances with the same style as extension functions are defined | ||
|
|
||
| ```kotlin | ||
| typeclass Monoid { | ||
| fun combine(b: Self): Self | ||
| fun Self.Companion.empty(): Self | ||
| } | ||
| ``` | ||
|
|
||
| The above declaration can serve as target for implementations for any arbitrary datatype. | ||
| In the implementation below we provide evidence that there is an `Int: Monoid` instance that enables `combine` and `empty` on `Int` | ||
|
|
||
| ```kotlin | ||
| extension Int : Monoid { | ||
| fun combine(b: Int): Int = this + b | ||
| fun Int.Companion.empty(): Int = 0 | ||
| } | ||
|
|
||
| 1.combine(2) // 3 | ||
| Int.empty() // 0 | ||
| ``` | ||
|
|
||
| Because of this constrain where we are stating that there is a `Monoid` constrain for a given type `A` we can also encode polymorphic definitions based on those constrains: | ||
|
|
||
| ```kotlin | ||
| fun <A : Monoid> add(a: A, b: A): A = a.combine(b) | ||
| add(1, 1) // compiles | ||
| add("a", "b") // does not compile: No `String: Monoid` instance defined in scope | ||
| ``` | ||
|
|
||
| On top of the value this brings to typed FP in Kotlin it also helps in OOP contexts where dependencies can be provided at compile time: | ||
|
|
||
| ```kotlin | ||
| typeclass Context { | ||
| fun config(): Config | ||
| } | ||
| ``` | ||
|
|
||
| ```kotlin | ||
| package prod | ||
|
|
||
| extension Service: Context { | ||
| fun config(): Config = ProdConfig | ||
| } | ||
| ``` | ||
|
|
||
| ```kotlin | ||
| package test | ||
|
|
||
| extension Service: Context { | ||
| fun config(): Config = TestConfig | ||
| } | ||
| ``` | ||
|
|
||
| ```kotlin | ||
| package prod | ||
|
|
||
| service.config() // ProdConfig | ||
| ``` | ||
|
|
||
| ```kotlin | ||
| package test | ||
|
|
||
| service.config() // TestConfig | ||
| ``` | ||
|
|
||
| Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: | ||
|
|
||
| ```kotlin | ||
| extension Option<A: Monoid> : Monoid { | ||
|
|
||
| fun Option.Companion.empty(): Option<A> = None | ||
|
|
||
| fun combine(ob: Option<A>): Option<A> = | ||
| when (this) { | ||
| is Some<A> -> when (ob) { | ||
| is Some<A> -> Some(this.value.combine(b.value)) | ||
| is None -> ob | ||
| } | ||
| is None -> this | ||
| } | ||
|
|
||
| } | ||
| ``` | ||
|
|
||
| The above instance declares a `Monoid: Option<A>` as long as there is a `A: Monoid` in scope. | ||
|
|
||
| ```kotlin | ||
| Option(1).combine(Option(1)) // Option(2) | ||
| Option("a").combine(Option("b")) // does not compile. Found `Option<A>: Monoid` instance providing `combine` but no `String: Monoid` instance was in scope | ||
| ``` | ||
|
|
||
| We believe the above proposed encoding fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support typeclasses such as Scala where this is done via implicits. | ||
|
|
||
| # Typeclasses over type constructors | ||
|
|
||
| We recommend if this proposal is accepted that a lightweight version of higher kinds support is included to unveil the true power of typeclasses through the extensions mechanisms | ||
|
|
||
| A syntax that would allow for higher kinds in these definitions may look like this: | ||
|
|
||
| ```kotlin | ||
| typeclass FunctionK<F<_>, G<_>> { | ||
| fun <A> invoke(fa: F<A>): G<A> | ||
| } | ||
|
|
||
| extension Option2List : FunctionK<Option, List> { | ||
| fun <A> invoke(fa: Option<A>): List<A> = | ||
| fa.fold({ emptyList() }, { listOf(it) }) | ||
| } | ||
| ``` | ||
|
|
||
| If Higher Kinds where added along with typeclasses to the lang an alternate definition to the encoding below: | ||
|
|
||
| ```kotlin | ||
| typeclass Functor { | ||
| fun map(b: Self): Self | ||
| } | ||
| ``` | ||
|
|
||
| could be provided such as: | ||
|
|
||
| ```kotlin | ||
| typeclass F<_> : Functor { | ||
| fun <A, B> map(fa: F<A>, f: (A) -> B): F<B> | ||
| } | ||
|
|
||
| extension Option: Functor { | ||
| fun <A, B> map(fa: Option<A>, f: (A) -> B): Option<B> | ||
| } | ||
| ``` | ||
|
|
||
| Here `F<_>` refers to a type that has a hole on it such as `Option`, `List`, etc. | ||
|
|
||
| A use of this declaration in a polymorphic function would look like: | ||
|
|
||
| ```kotlin | ||
| fun <F<_> : Functor, A, B> transform(fa: F<A>, f: (A) -> B): F<B> = F.map(fa, f) | ||
|
|
||
| transform(Option(1), { it + 1 }) // Option(2) | ||
| transform("", { it + "b" }) // Does not compile: `String` is not type constructor with shape F<_> | ||
| transform(listOf(1), { it + 1 }) // does not compile: No `List<_>: Functor` instance defined in scope. | ||
| ``` | ||
|
|
||
| Once typeclasses extensions are defined over datatypes the compiler could automatically | ||
| add extensions to the target datatypes unless the target datatype already defines a method with the same signature: | ||
|
|
||
| ```kotlin | ||
| typeclass F<_> : Functor { | ||
| fun <A, B> map(fa: F<A>, f: (A) -> B): F<B> | ||
| fun <A, B> Self.Companion.lift(f: (A) -> B): (F<A>) -> F<B> | ||
| } | ||
|
|
||
|
||
| extension Option: Functor { | ||
| fun <A, B> map(fa: Option<A>, f: (A) -> B): Option<B> = ... //does not enable `Option(1).map(Option(1)) becase `Option#map` already exists with the same signature as an instance method | ||
| fun <A, B> Option.Companion.lift(f: (A) -> B): (Option<A>) -> Option<B> = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option<Int> -> Option<String> | ||
| } | ||
| ``` | ||
|
|
||
| Some of this examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://kotlinlang.slack.com/archives/C1JMF6UDV/p1506897887000023 | ||
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty()isn't an extension function ofInt.Companion, this line should be replaced byIntMonoind.empty().