From 13df9dd9ab9c75e04a6e77c8085cf403d3a01f9e Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 2 Oct 2017 21:24:54 +0200 Subject: [PATCH 01/73] Type Classes via natural extensions in Kotlin --- proposals/type-classes.md | 182 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 proposals/type-classes.md diff --git a/proposals/type-classes.md b/proposals/type-classes.md new file mode 100644 index 000000000..52edaf22d --- /dev/null +++ b/proposals/type-classes.md @@ -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 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 : Monoid { + + fun Option.Companion.empty(): Option = None + + fun combine(ob: Option): Option = + when (this) { + is Some -> when (ob) { + is Some -> Some(this.value.combine(b.value)) + is None -> ob + } + is None -> this + } + +} +``` + +The above instance declares a `Monoid: Option` 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: 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, G<_>> { + fun invoke(fa: F): G +} + +extension Option2List : FunctionK { + fun invoke(fa: Option): List = + 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 map(fa: F, f: (A) -> B): F +} + +extension Option: Functor { + fun map(fa: Option, f: (A) -> B): Option +} +``` + +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 : Functor, A, B> transform(fa: F, f: (A) -> B): F = 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 map(fa: F, f: (A) -> B): F + fun Self.Companion.lift(f: (A) -> B): (F) -> F +} + +extension Option: Functor { + fun map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) becase `Option#map` already exists with the same signature as an instance method + fun Option.Companion.lift(f: (A) -> B): (Option) -> Option = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option -> Option +} +``` + +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 \ No newline at end of file From 7494c84af180a00cd91e6bd0f86f3c7db0758757 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 2 Oct 2017 22:35:07 +0200 Subject: [PATCH 02/73] Adapted code examples to new proposed syntax https://github.com/Kotlin/KEEP/pull/87#issuecomment-333653188 --- proposals/type-classes.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 52edaf22d..cc15fdc60 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -25,8 +25,8 @@ We propose to introduce a new top level declaration `typeclass` that allows for ```kotlin typeclass Monoid { - fun combine(b: Self): Self - fun Self.Companion.empty(): Self + fun Self.combine(b: Self): Self + fun empty(): Self } ``` @@ -35,8 +35,8 @@ In the implementation below we provide evidence that there is an `Int: Monoid` i ```kotlin extension Int : Monoid { - fun combine(b: Int): Int = this + b - fun Int.Companion.empty(): Int = 0 + fun Int.combine(b: Int): Int = this + b + fun empty(): Int = 0 } 1.combine(2) // 3 @@ -55,7 +55,7 @@ On top of the value this brings to typed FP in Kotlin it also helps in OOP conte ```kotlin typeclass Context { - fun config(): Config + fun Self.config(): Config } ``` @@ -63,7 +63,7 @@ typeclass Context { package prod extension Service: Context { - fun config(): Config = ProdConfig + fun Service.config(): Config = ProdConfig } ``` @@ -71,7 +71,7 @@ extension Service: Context { package test extension Service: Context { - fun config(): Config = TestConfig + fun Service.config(): Config = TestConfig } ``` @@ -92,9 +92,9 @@ Type class instances and declarations can encode further constrains in their gen ```kotlin extension Option : Monoid { - fun Option.Companion.empty(): Option = None + fun empty(): Option = None - fun combine(ob: Option): Option = + fun Option.combine(ob: Option): Option = when (this) { is Some -> when (ob) { is Some -> Some(this.value.combine(b.value)) @@ -136,7 +136,7 @@ If Higher Kinds where added along with typeclasses to the lang an alternate defi ```kotlin typeclass Functor { - fun map(b: Self): Self + fun Self.map(b: Self): Self } ``` @@ -174,8 +174,8 @@ typeclass F<_> : Functor { } extension Option: Functor { - fun map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) becase `Option#map` already exists with the same signature as an instance method - fun Option.Companion.lift(f: (A) -> B): (Option) -> Option = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option -> Option + fun Self.map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) becase `Option#map` already exists with the same signature as an instance method + fun lift(f: (A) -> B): (Option) -> Option = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option -> Option } ``` From a5f9659cd42cbf99399d5490d3296df6ebc1ae4d Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Tue, 3 Oct 2017 10:02:29 +0200 Subject: [PATCH 03/73] Fixed typo --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index cc15fdc60..89f92f259 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -174,7 +174,7 @@ typeclass F<_> : Functor { } extension Option: Functor { - fun Self.map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) becase `Option#map` already exists with the same signature as an instance method + fun Self.map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) because `Option#map` already exists with the same signature as an instance method fun lift(f: (A) -> B): (Option) -> Option = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option -> Option } ``` From de8032b41ebfc98df5d2e61af3db3965759701fc Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Wed, 4 Oct 2017 10:14:26 +0200 Subject: [PATCH 04/73] Included section on overcoming `inline` `reified` limitations as shown in https://github.com/Kotlin/KEEP/pull/87#issuecomment-333915196 --- proposals/type-classes.md | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 89f92f259..c15245ab3 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -11,6 +11,7 @@ The goal of this proposal is to enable `typeclasses` and lightweight `Higher Kin 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. +Furthermore introduction of typeclasses usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. ## Motivation @@ -18,6 +19,7 @@ dependency injection which will improve the current landscape where trivial appl * 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 +* Enable alternative for inline reified generics ## Description @@ -51,6 +53,8 @@ add(1, 1) // compiles add("a", "b") // does not compile: No `String: Monoid` instance defined in scope ``` +## Compile Time Dependency Injection + 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 @@ -87,6 +91,48 @@ package test service.config() // TestConfig ``` +## Overcoming `inline` + `reified` limitations + +Typeclasses allow us to workaround `inline` `reified` generics and their limitations and express those as typeclasses instead it: + +```kotlin +typeclass Reified { + val selfClass: KClass +} +``` + +Now a function that was doing something like: + +```kotlin +inline fun foo() { .... T::class ... } +``` + +can be replaced with: + +```kotlin +fun fooTC() { .... T.selfClass ... } +``` + +This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions. + +Currently `inline fun foo()` can't be called in non reified context such as: + +```kotlin +class MyClass { + fun doFoo() = foo() //fails to compile because T is not reified. +} +``` + +because `T` is not reified the compiler won't allow this position. With type classes this will be possible: + +```kotlin +class MyClass { + fun doFoo() = fooTC() // Compiles because there is evidence that there is an instance of `Reified` for `T` +} +``` + +## Composition and chain of evidences + Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: ```kotlin @@ -115,7 +161,7 @@ Option("a").combine(Option("b")) // does not compile. Found `Option: Monoid` 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 +## 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 @@ -164,19 +210,4 @@ transform("", { it + "b" }) // Does not compile: `String` is not type constructo 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 map(fa: F, f: (A) -> B): F - fun Self.Companion.lift(f: (A) -> B): (F) -> F -} - -extension Option: Functor { - fun Self.map(fa: Option, f: (A) -> B): Option = ... //does not enable `Option(1).map(Option(1)) because `Option#map` already exists with the same signature as an instance method - fun lift(f: (A) -> B): (Option) -> Option = ... //enables Option.lift({n: Int -> n.toString() }) because the Option companion does not define `lift` // Option -> Option -} -``` - 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 \ No newline at end of file From 47e13484b6191c6696bb1c5de2dc116b38155133 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 9 Oct 2017 21:13:49 +0200 Subject: [PATCH 05/73] Adapt proposal examples to new style using `given` --- proposals/type-classes.md | 84 ++++++++++++++------------------------- 1 file changed, 30 insertions(+), 54 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index c15245ab3..cb1b9b9d0 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -23,20 +23,20 @@ Furthermore introduction of typeclasses usages for `reified` generic functions w ## 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 +We propose to use the existing `interface` semantics allowing for generic definition of typeclasses and their instances with the same style interfaces are defined ```kotlin -typeclass Monoid { - fun Self.combine(b: Self): Self - fun empty(): Self +interface Monoid { + fun A.combine(b: A): A + fun empty(): A } ``` 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` +In the implementation below we provide evidence that there is a `Monoid` instance that enables `combine` and `empty` on `Int` ```kotlin -extension Int : Monoid { +extension class IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b fun empty(): Int = 0 } @@ -48,7 +48,7 @@ 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 add(a: A, b: A): A = a.combine(b) +fun add(a: A, b: A): A given Monoid = a.combine(b) add(1, 1) // compiles add("a", "b") // does not compile: No `String: Monoid` instance defined in scope ``` @@ -58,15 +58,15 @@ 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 Self.config(): Config +interface Context { + fun A.config(): Config } ``` ```kotlin package prod -extension Service: Context { +extension class ProdContext: Context { fun Service.config(): Config = ProdConfig } ``` @@ -74,7 +74,7 @@ extension Service: Context { ```kotlin package test -extension Service: Context { +extension class TestContext: Context { fun Service.config(): Config = TestConfig } ``` @@ -93,41 +93,37 @@ service.config() // TestConfig ## Overcoming `inline` + `reified` limitations -Typeclasses allow us to workaround `inline` `reified` generics and their limitations and express those as typeclasses instead it: +Typeclasses allow us to workaround `inline` `reified` generics and their limitations and express those as typeclasses instead: ```kotlin -typeclass Reified { - val selfClass: KClass +interface Reified { + val selfClass: KClass } ``` Now a function that was doing something like: ```kotlin -inline fun foo() { .... T::class ... } +inline fun foo() { .... A::class ... } ``` can be replaced with: ```kotlin -fun fooTC() { .... T.selfClass ... } +fun fooTC(): Klass given Reified { .... T.selfClass ... } ``` -This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions. - -Currently `inline fun foo()` can't be called in non reified context such as: +This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. ```kotlin -class MyClass { - fun doFoo() = foo() //fails to compile because T is not reified. +class Foo { + val someKlazz = foo() //won't compile because class disallow reified type args. } ``` -because `T` is not reified the compiler won't allow this position. With type classes this will be possible: - ```kotlin -class MyClass { - fun doFoo() = fooTC() // Compiles because there is evidence that there is an instance of `Reified` for `T` +class Foo { + val someKlazz = fooTC() //works and has no reflection runtime overhead } ``` @@ -136,14 +132,14 @@ class MyClass { Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: ```kotlin -extension Option : Monoid { +extension class OptionMonoid : Monoid> given Monoid { fun empty(): Option = None fun Option.combine(ob: Option): Option = when (this) { is Some -> when (ob) { - is Some -> Some(this.value.combine(b.value)) + is Some -> Some(this.value.combine(b.value)) //works because there is evidence of a Monoid is None -> ob } is None -> this @@ -152,11 +148,11 @@ extension Option : Monoid { } ``` -The above instance declares a `Monoid: Option` as long as there is a `A: Monoid` in scope. +The above instance declares a `Monoid>` as long as there is a `Monoid` in scope. ```kotlin Option(1).combine(Option(1)) // Option(2) -Option("a").combine(Option("b")) // does not compile. Found `Option: Monoid` instance providing `combine` but no `String: Monoid` instance was in scope +Option("a").combine(Option("b")) // does not compile. Found `Monoid>` instance providing `combine` but no `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. @@ -168,46 +164,26 @@ We recommend if this proposal is accepted that a lightweight version of higher k A syntax that would allow for higher kinds in these definitions may look like this: ```kotlin -typeclass FunctionK, G<_>> { +interface FunctionK, G<_>> { fun invoke(fa: F): G } -extension Option2List : FunctionK { +extension class Option2List : FunctionK { fun invoke(fa: Option): List = 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 Self.map(b: Self): Self -} -``` - -could be provided such as: - -```kotlin -typeclass F<_> : Functor { - fun map(fa: F, f: (A) -> B): F -} - -extension Option: Functor { - fun map(fa: Option, f: (A) -> B): Option -} -``` - 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 : Functor, A, B> transform(fa: F, f: (A) -> B): F = F.map(fa, f) +fun , A, B> transform(fa: F, f: (A) -> B): F given Functor = 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. +transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instance defined in scope. ``` -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 \ No newline at end of file +Some of this examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 \ No newline at end of file From eb4f68750fb96b5c9b0bbb746e0fbda8be2550ca Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Tue, 10 Oct 2017 22:46:00 +0200 Subject: [PATCH 06/73] replace `extension class` for `extension object` where possible addressing https://github.com/Kotlin/KEEP/pull/87#issuecomment-335527870 --- proposals/type-classes.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index cb1b9b9d0..3c9f053ec 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -36,7 +36,7 @@ The above declaration can serve as target for implementations for any arbitrary In the implementation below we provide evidence that there is a `Monoid` instance that enables `combine` and `empty` on `Int` ```kotlin -extension class IntMonoid : Monoid { +extension object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b fun empty(): Int = 0 } @@ -66,7 +66,7 @@ interface Context { ```kotlin package prod -extension class ProdContext: Context { +extension object ProdContext: Context { fun Service.config(): Config = ProdConfig } ``` @@ -74,7 +74,7 @@ extension class ProdContext: Context { ```kotlin package test -extension class TestContext: Context { +extension object TestContext: Context { fun Service.config(): Config = TestConfig } ``` @@ -132,7 +132,7 @@ class Foo { Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: ```kotlin -extension class OptionMonoid : Monoid> given Monoid { +extension class OptionMonoid : Monoid> given Monoid { fun empty(): Option = None @@ -168,7 +168,7 @@ interface FunctionK, G<_>> { fun invoke(fa: F): G } -extension class Option2List : FunctionK { +object Option2List : FunctionK { fun invoke(fa: Option): List = fa.fold({ emptyList() }, { listOf(it) }) } From 3777fa6c32b4b69746b84b1750159b38c76958e2 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Tue, 10 Oct 2017 22:57:35 +0200 Subject: [PATCH 07/73] added imports to code examples --- proposals/type-classes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 3c9f053ec..d4b08ec26 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -36,10 +36,16 @@ The above declaration can serve as target for implementations for any arbitrary In the implementation below we provide evidence that there is a `Monoid` instance that enables `combine` and `empty` on `Int` ```kotlin +package intext + extension object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b fun empty(): Int = 0 } +``` + +``` +import intext.IntMonoid 1.combine(2) // 3 Int.empty() // 0 @@ -48,6 +54,8 @@ 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 +import intext.IntMonoid + fun add(a: A, b: A): A given Monoid = a.combine(b) add(1, 1) // compiles add("a", "b") // does not compile: No `String: Monoid` instance defined in scope @@ -132,6 +140,8 @@ class Foo { Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: ```kotlin +package optionext + extension class OptionMonoid : Monoid> given Monoid { fun empty(): Option = None @@ -151,6 +161,9 @@ extension class OptionMonoid : Monoid> given Monoid { The above instance declares a `Monoid>` as long as there is a `Monoid` in scope. ```kotlin +import optionext.OptionMonoid +import intext.IntMonoid + Option(1).combine(Option(1)) // Option(2) Option("a").combine(Option("b")) // does not compile. Found `Monoid>` instance providing `combine` but no `Monoid` instance was in scope ``` From 374e8489c0ed185aed7597e8ddb0d4212a27d58f Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Thu, 12 Oct 2017 22:57:16 +0200 Subject: [PATCH 08/73] Add language changes and instance resolution rules order --- proposals/type-classes.md | 130 +++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 17 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index d4b08ec26..657c28762 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -7,23 +7,24 @@ ## 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. +The goal of this proposal is to enable `type classes` 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. -Furthermore introduction of typeclasses usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. +Furthermore introduction of type classes improves usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. ## 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 -* Enable alternative for inline reified generics +* Support Type class 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 Type class pattern. +* Enable better compile reified generics without the need for explicit inlining. +* Enable definition of polymorphic functions whose constrains can be verified at compile time in call sites. ## Description -We propose to use the existing `interface` semantics allowing for generic definition of typeclasses and their instances with the same style interfaces are defined +We propose to use the existing `interface` semantics allowing for generic definition of type classes and their instances with the same style interfaces are defined ```kotlin interface Monoid { @@ -32,13 +33,13 @@ interface Monoid { } ``` -The above declaration can serve as target for implementations for any arbitrary datatype. +The above declaration can serve as target for implementations for any arbitrary data type. In the implementation below we provide evidence that there is a `Monoid` instance that enables `combine` and `empty` on `Int` ```kotlin package intext -extension object IntMonoid : Monoid { +object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b fun empty(): Int = 0 } @@ -74,7 +75,7 @@ interface Context { ```kotlin package prod -extension object ProdContext: Context { +object ProdContext: Context { fun Service.config(): Config = ProdConfig } ``` @@ -82,7 +83,7 @@ extension object ProdContext: Context { ```kotlin package test -extension object TestContext: Context { +object TestContext: Context { fun Service.config(): Config = TestConfig } ``` @@ -101,7 +102,7 @@ service.config() // TestConfig ## Overcoming `inline` + `reified` limitations -Typeclasses allow us to workaround `inline` `reified` generics and their limitations and express those as typeclasses instead: +Type classes allow us to workaround `inline` `reified` generics and their limitations and express those as type classes instead: ```kotlin interface Reified { @@ -131,7 +132,7 @@ class Foo { ```kotlin class Foo { - val someKlazz = fooTC() //works and has no reflection runtime overhead + val someKlazz = fooTC() //works anddoes not requires to be inside an `inline reified` context. } ``` @@ -142,7 +143,7 @@ Type class instances and declarations can encode further constrains in their gen ```kotlin package optionext -extension class OptionMonoid : Monoid> given Monoid { +class OptionMonoid : Monoid> given Monoid { fun empty(): Option = None @@ -187,7 +188,7 @@ object Option2List : FunctionK { } ``` -Here `F<_>` refers to a type that has a hole on it such as `Option`, `List`, etc. +Here `F<_>` refers to a type constructor meaning 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: @@ -199,4 +200,99 @@ transform("", { it + "b" }) // Does not compile: `String` is not type constructo transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instance defined in scope. ``` -Some of this examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 \ No newline at end of file +## Language Changes + +- Add `given` to require instances evidences in both function and interface/class declarations as demonstrated by previous and below examples: +```kotlin +class OptionMonoid : Monoid> given Monoid //class position + +fun add(a: A, b: A): A given Monoid = a.combine(b) //function position +``` + +The below alternative approach to `given` using parameters and the special keyword `instance` was also proposed but discarded since `given` +was more inline with other similar usages such as `where` that users are already used to and did not require to name the instances to activate extension syntax. + +```kotlin +class OptionMonoid(instance MA: Monoid) : Monoid> //class position + +fun add(a: A, b: A, instance MA: Monoid): A = a.combine(b) //function position +``` + +## Compiler Changes + +- The type checker will declare the below definition as valid since the `given` clause provides evidence that call sites won't be able to compile calls to this function unless a `Monoid` is in scope. +```kotlin +fun add(a: A, b: A): A given Monoid = a.combine(b) //compiles +``` +- The type checker will declare the below definition as invalid since there is no `Monoid` in scope. +```kotlin +add(1, 2) +``` +- The type checker will declare the below definition as valid since there is a `Monoid` in scope. +```kotlin +import intext.IntMonoid +add(1, 2) +``` +- The type checker will declare the below definition as valid since there is a `Monoid` in scope. +```kotlin +fun addInts(a: Int, b: Int): Int given Monoid = add(a, b) +``` +- The type checker will declare the below definition as valid since there is a `with` block around the concrete `IntMonoid` in scope. +```kotlin +fun addInts(a: Int, b: Int): Int = with(IntMonoid) { add(a, b) } +``` + +## Compile resolution rules + +When the compiler finds a call site invoking a function that has type class instances constrains declared with `given` as in the example below: + +Declaration: +```kotlin +fun add(a: A, b: A): A given Monoid = a.combine(b) +``` +Call site: +```kotlin +class AddingInts { + fun addInts(): Int = add(1, 2) +} +``` +The compiler may choose the following order for resolving the evidence that a `Monoid` exists in scope. + +1. Look in the most immediate scope for declarations of `given Monoid` in this case the function `addInts` + +This will compile because the responsibility of providing `Monoid` is passed unto the callers of `addInts()`: +```kotlin +class AddingInts { + fun addInts(): Int given Monoid = add(1, 2) +} +``` + +2. Look in the most outher class/interface scope for declarations of `given Monoid` in this case the class `AddingInts`: +```kotlin +class AddingInts given Monoid { + fun addInts(): Int = add(1, 2) +} +``` +This will compile because the responsibility of providing `Monoid` is passed unto the callers of `AddingInts()` + +3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid`: +```kotlin +import intext.IntMonoid +class AddingInts { + fun addInts(): Int = add(1, 2) +} +``` +This will compile because the responsibility of providing `Monoid` is satisfied by `import intext.IntMonoid` + +4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. +```kotlin +import intext.IntMonoid +class AddingInts { + fun addInts(): Int = add(1, 2) +} +``` +Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`.: + + + +Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 \ No newline at end of file From 2f2f27ebbab143933a1d63fcdaf40c8292cd2f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 24 Oct 2017 22:00:50 +0200 Subject: [PATCH 09/73] fixed wrong type param reference --- proposals/type-classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 657c28762..e06e4f003 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -119,7 +119,7 @@ inline fun foo() { .... A::class ... } can be replaced with: ```kotlin -fun fooTC(): Klass given Reified { .... T.selfClass ... } +fun fooTC(): Klass given Reified { .... A.selfClass ... } ``` This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. @@ -295,4 +295,4 @@ Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is -Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 \ No newline at end of file +Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 From 3b568bf0fb1ef71fdc55258cc21c273fdcac68c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 7 Nov 2017 19:21:32 +0000 Subject: [PATCH 10/73] code review comments on `most` vs `immediately` --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index e06e4f003..29f00d03e 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -267,7 +267,7 @@ class AddingInts { } ``` -2. Look in the most outher class/interface scope for declarations of `given Monoid` in this case the class `AddingInts`: +2. Look in the immediately outher class/interface scope for declarations of `given Monoid` in this case the class `AddingInts`: ```kotlin class AddingInts given Monoid { fun addInts(): Int = add(1, 2) From 7d109013f79a48c0cbf2f1d6ad57b90fd3eb9a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Tue, 7 Nov 2017 21:16:46 +0000 Subject: [PATCH 11/73] Reverted to use `typeclass` and `instance` Based on https://github.com/Kotlin/KEEP/pull/87#discussion_r149380034 reverting to be explicit so there is no ambiguity as to what type classes and their instances are --- proposals/type-classes.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 29f00d03e..5b42d424d 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -27,7 +27,7 @@ Furthermore introduction of type classes improves usages for `reified` generic f We propose to use the existing `interface` semantics allowing for generic definition of type classes and their instances with the same style interfaces are defined ```kotlin -interface Monoid { +typeclass Monoid { fun A.combine(b: A): A fun empty(): A } @@ -39,7 +39,7 @@ In the implementation below we provide evidence that there is a `Monoid` in ```kotlin package intext -object IntMonoid : Monoid { +instance object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b fun empty(): Int = 0 } @@ -67,7 +67,7 @@ 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 -interface Context { +typeclass Context { fun A.config(): Config } ``` @@ -75,7 +75,7 @@ interface Context { ```kotlin package prod -object ProdContext: Context { +instance object ProdContext: Context { fun Service.config(): Config = ProdConfig } ``` @@ -83,7 +83,7 @@ object ProdContext: Context { ```kotlin package test -object TestContext: Context { +instance object TestContext: Context { fun Service.config(): Config = TestConfig } ``` @@ -105,7 +105,7 @@ service.config() // TestConfig Type classes allow us to workaround `inline` `reified` generics and their limitations and express those as type classes instead: ```kotlin -interface Reified { +typeclass Reified { val selfClass: KClass } ``` @@ -125,13 +125,13 @@ fun fooTC(): Klass given Reified { .... A.selfClass ... } This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. ```kotlin -class Foo { +instance class Foo { val someKlazz = foo() //won't compile because class disallow reified type args. } ``` ```kotlin -class Foo { +instance class Foo { val someKlazz = fooTC() //works anddoes not requires to be inside an `inline reified` context. } ``` @@ -143,7 +143,7 @@ Type class instances and declarations can encode further constrains in their gen ```kotlin package optionext -class OptionMonoid : Monoid> given Monoid { +instance class OptionMonoid : Monoid> given Monoid { fun empty(): Option = None @@ -178,11 +178,11 @@ We recommend if this proposal is accepted that a lightweight version of higher k A syntax that would allow for higher kinds in these definitions may look like this: ```kotlin -interface FunctionK, G<_>> { +typeclass FunctionK, G<_>> { fun invoke(fa: F): G } -object Option2List : FunctionK { +instance object Option2List : FunctionK { fun invoke(fa: Option): List = fa.fold({ emptyList() }, { listOf(it) }) } @@ -204,7 +204,7 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc - Add `given` to require instances evidences in both function and interface/class declarations as demonstrated by previous and below examples: ```kotlin -class OptionMonoid : Monoid> given Monoid //class position +instance class OptionMonoid : Monoid> given Monoid //class position fun add(a: A, b: A): A given Monoid = a.combine(b) //function position ``` @@ -213,7 +213,7 @@ The below alternative approach to `given` using parameters and the special keywo was more inline with other similar usages such as `where` that users are already used to and did not require to name the instances to activate extension syntax. ```kotlin -class OptionMonoid(instance MA: Monoid) : Monoid> //class position +instance class OptionMonoid(instance MA: Monoid) : Monoid> //class position fun add(a: A, b: A, instance MA: Monoid): A = a.combine(b) //function position ``` @@ -252,7 +252,7 @@ fun add(a: A, b: A): A given Monoid = a.combine(b) ``` Call site: ```kotlin -class AddingInts { +instance class AddingInts { fun addInts(): Int = add(1, 2) } ``` @@ -262,14 +262,14 @@ The compiler may choose the following order for resolving the evidence that a `M This will compile because the responsibility of providing `Monoid` is passed unto the callers of `addInts()`: ```kotlin -class AddingInts { +instance class AddingInts { fun addInts(): Int given Monoid = add(1, 2) } ``` 2. Look in the immediately outher class/interface scope for declarations of `given Monoid` in this case the class `AddingInts`: ```kotlin -class AddingInts given Monoid { +instance class AddingInts given Monoid { fun addInts(): Int = add(1, 2) } ``` @@ -278,7 +278,7 @@ This will compile because the responsibility of providing `Monoid` is passe 3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid`: ```kotlin import intext.IntMonoid -class AddingInts { +instance class AddingInts { fun addInts(): Int = add(1, 2) } ``` @@ -287,7 +287,7 @@ This will compile because the responsibility of providing `Monoid` is satis 4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. ```kotlin import intext.IntMonoid -class AddingInts { +instance class AddingInts { fun addInts(): Int = add(1, 2) } ``` From f8dce435a3b48db1268274ce4361b40828326651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Sun, 12 Nov 2017 16:47:15 +0000 Subject: [PATCH 12/73] Added sentence clarifying the Reified example. --- proposals/type-classes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 5b42d424d..8d656782c 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -124,6 +124,8 @@ fun fooTC(): Klass given Reified { .... A.selfClass ... } This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. +Not this does not remove the need to use `inline reified` where one tries to instrospect generic type information at runtime with reflection. This particular case is only relevant for those cases where you know the types you want `Reified` ahead of time and you need to access to their class value. + ```kotlin instance class Foo { val someKlazz = foo() //won't compile because class disallow reified type args. From 323a55004fb19a556597f9966528b68215e30f7b Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Mon, 19 Feb 2018 07:29:51 +0100 Subject: [PATCH 13/73] `Monoid.empty` is a value --- proposals/type-classes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 8d656782c..43ccdf3ed 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -29,7 +29,7 @@ We propose to use the existing `interface` semantics allowing for generic defini ```kotlin typeclass Monoid { fun A.combine(b: A): A - fun empty(): A + val empty: A } ``` @@ -41,7 +41,7 @@ package intext instance object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b - fun empty(): Int = 0 + val empty: Int = 0 } ``` @@ -49,7 +49,7 @@ instance object IntMonoid : Monoid { import intext.IntMonoid 1.combine(2) // 3 -Int.empty() // 0 +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: @@ -147,7 +147,7 @@ package optionext instance class OptionMonoid : Monoid> given Monoid { - fun empty(): Option = None + val empty: Option = None fun Option.combine(ob: Option): Option = when (this) { From a1054965522f4277baa2152bb4b5cc1dcb3fbc23 Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Tue, 27 Feb 2018 18:59:35 +0100 Subject: [PATCH 14/73] Use IntMonoind in the example --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 8d656782c..262e01e97 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -49,7 +49,7 @@ instance object IntMonoid : Monoid { import intext.IntMonoid 1.combine(2) // 3 -Int.empty() // 0 +IntMonoind.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: From 505619c3fc06866c762fc0c4f3c1766aa6aff890 Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Tue, 27 Feb 2018 19:02:04 +0100 Subject: [PATCH 15/73] Specify package in 'with' --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 8d656782c..e2004380c 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -241,7 +241,7 @@ fun addInts(a: Int, b: Int): Int given Monoid = add(a, b) ``` - The type checker will declare the below definition as valid since there is a `with` block around the concrete `IntMonoid` in scope. ```kotlin -fun addInts(a: Int, b: Int): Int = with(IntMonoid) { add(a, b) } +fun addInts(a: Int, b: Int): Int = with(intext.IntMonoid) { add(a, b) } ``` ## Compile resolution rules From 115a48344e1b63f17c76e665823babebdd4644f0 Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Tue, 27 Feb 2018 19:03:58 +0100 Subject: [PATCH 16/73] Remove extra import in example 4 --- proposals/type-classes.md | 1 - 1 file changed, 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 8d656782c..2f721cb7f 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -288,7 +288,6 @@ This will compile because the responsibility of providing `Monoid` is satis 4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. ```kotlin -import intext.IntMonoid instance class AddingInts { fun addInts(): Int = add(1, 2) } From 4a98c86a023664ce3eaeeeabcfa92a948c3407bb Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Tue, 10 Apr 2018 22:41:15 +0200 Subject: [PATCH 17/73] Using extension keyword --- proposals/type-classes.md | 81 +++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index dd0eea983..20547d679 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -27,7 +27,7 @@ Furthermore introduction of type classes improves usages for `reified` generic f We propose to use the existing `interface` semantics allowing for generic definition of type classes and their instances with the same style interfaces are defined ```kotlin -typeclass Monoid { +extension interface Monoid { fun A.combine(b: A): A val empty: A } @@ -39,7 +39,7 @@ In the implementation below we provide evidence that there is a `Monoid` in ```kotlin package intext -instance object IntMonoid : Monoid { +extension object IntMonoid : Monoid { fun Int.combine(b: Int): Int = this + b val empty: Int = 0 } @@ -57,7 +57,7 @@ Because of this constrain where we are stating that there is a `Monoid` constrai ```kotlin import intext.IntMonoid -fun add(a: A, b: A): A given Monoid = a.combine(b) +fun add(a: A, b: A, with Monoid): A = a.combine(b) add(1, 1) // compiles add("a", "b") // does not compile: No `String: Monoid` instance defined in scope ``` @@ -67,7 +67,7 @@ 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 { +extension interface Context { fun A.config(): Config } ``` @@ -75,7 +75,7 @@ typeclass Context { ```kotlin package prod -instance object ProdContext: Context { +extension object ProdContext: Context { fun Service.config(): Config = ProdConfig } ``` @@ -83,7 +83,7 @@ instance object ProdContext: Context { ```kotlin package test -instance object TestContext: Context { +extension object TestContext: Context { fun Service.config(): Config = TestConfig } ``` @@ -105,8 +105,8 @@ service.config() // TestConfig Type classes allow us to workaround `inline` `reified` generics and their limitations and express those as type classes instead: ```kotlin -typeclass Reified { - val selfClass: KClass +extension interface Reified { + val A.selfClass: KClass } ``` @@ -119,7 +119,7 @@ inline fun foo() { .... A::class ... } can be replaced with: ```kotlin -fun fooTC(): Klass given Reified { .... A.selfClass ... } +fun fooTC(with Reified): Klass { .... A.selfClass ... } ``` This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. @@ -127,14 +127,14 @@ This allows us to obtain generics info without the need to declare the functions Not this does not remove the need to use `inline reified` where one tries to instrospect generic type information at runtime with reflection. This particular case is only relevant for those cases where you know the types you want `Reified` ahead of time and you need to access to their class value. ```kotlin -instance class Foo { +extension class Foo { val someKlazz = foo() //won't compile because class disallow reified type args. } ``` ```kotlin -instance class Foo { - val someKlazz = fooTC() //works anddoes not requires to be inside an `inline reified` context. +extension class Foo { + val someKlazz = fooTC() //works and does not requires to be inside an `inline reified` context. } ``` @@ -145,7 +145,7 @@ Type class instances and declarations can encode further constrains in their gen ```kotlin package optionext -instance class OptionMonoid : Monoid> given Monoid { +extension class OptionMonoid(with Monoid): Monoid> { val empty: Option = None @@ -180,11 +180,11 @@ We recommend if this proposal is accepted that a lightweight version of higher k A syntax that would allow for higher kinds in these definitions may look like this: ```kotlin -typeclass FunctionK, G<_>> { +extension interface FunctionK, G<_>> { fun invoke(fa: F): G } -instance object Option2List : FunctionK { +extension object Option2List : FunctionK { fun invoke(fa: Option): List = fa.fold({ emptyList() }, { listOf(it) }) } @@ -195,7 +195,7 @@ Here `F<_>` refers to a type constructor meaning a type that has a hole on it su A use of this declaration in a polymorphic function would look like: ```kotlin -fun , A, B> transform(fa: F, f: (A) -> B): F given Functor = F.map(fa, f) +fun , A, B> transform(fa: F, f: (A): B, with Functor): F = 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<_> @@ -204,27 +204,20 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc ## Language Changes -- Add `given` to require instances evidences in both function and interface/class declarations as demonstrated by previous and below examples: +- Add `with` to require instances evidences in both function and class/object declarations as demonstrated by previous and below examples: ```kotlin -instance class OptionMonoid : Monoid> given Monoid //class position +extension class OptionMonoid(with Monoid) : Monoid> //class position using argument "Monoid" +extension class OptionMonoid(with M: Monoid) : Monoid> //class position using argument "M" -fun add(a: A, b: A): A given Monoid = a.combine(b) //function position -``` - -The below alternative approach to `given` using parameters and the special keyword `instance` was also proposed but discarded since `given` -was more inline with other similar usages such as `where` that users are already used to and did not require to name the instances to activate extension syntax. - -```kotlin -instance class OptionMonoid(instance MA: Monoid) : Monoid> //class position - -fun add(a: A, b: A, instance MA: Monoid): A = a.combine(b) //function position +fun add(a: A, b: A, with Monoid): A = a.combine(b) //function position using argument "Monoid" +fun add(a: A, b: A, with M: Monoid): A = a.combine(b) //function position argument "M" ``` ## Compiler Changes -- The type checker will declare the below definition as valid since the `given` clause provides evidence that call sites won't be able to compile calls to this function unless a `Monoid` is in scope. +- The type checker will declare the below definition as valid since the `with` clause provides evidence that call sites won't be able to compile calls to this function unless a `Monoid` is in scope. ```kotlin -fun add(a: A, b: A): A given Monoid = a.combine(b) //compiles +fun add(a: A, b: A, with Monoid): A = a.combine(b) //compiles ``` - The type checker will declare the below definition as invalid since there is no `Monoid` in scope. ```kotlin @@ -237,7 +230,7 @@ add(1, 2) ``` - The type checker will declare the below definition as valid since there is a `Monoid` in scope. ```kotlin -fun addInts(a: Int, b: Int): Int given Monoid = add(a, b) +fun addInts(a: Int, b: Int, with Monoid): Int = add(a, b) ``` - The type checker will declare the below definition as valid since there is a `with` block around the concrete `IntMonoid` in scope. ```kotlin @@ -246,32 +239,32 @@ fun addInts(a: Int, b: Int): Int = with(intext.IntMonoid) { add(a, b) } ## Compile resolution rules -When the compiler finds a call site invoking a function that has type class instances constrains declared with `given` as in the example below: +When the compiler finds a call site invoking a function that has type class instances constrains declared with `with` as in the example below: Declaration: ```kotlin -fun add(a: A, b: A): A given Monoid = a.combine(b) +fun add(a: A, b: A, with Monoid): A = a.combine(b) ``` Call site: ```kotlin -instance class AddingInts { +extension class AddingInts { fun addInts(): Int = add(1, 2) } ``` The compiler may choose the following order for resolving the evidence that a `Monoid` exists in scope. -1. Look in the most immediate scope for declarations of `given Monoid` in this case the function `addInts` +1. Look in the most immediate scope for declarations of `with Monoid` in this case the function `addInts` -This will compile because the responsibility of providing `Monoid` is passed unto the callers of `addInts()`: +This will compile because the responsibility of providing `Monoid` is passed into the callers of `addInts()`: ```kotlin -instance class AddingInts { - fun addInts(): Int given Monoid = add(1, 2) +extension class AddingInts { + fun addInts(with Monoid): Int = add(1, 2) } ``` -2. Look in the immediately outher class/interface scope for declarations of `given Monoid` in this case the class `AddingInts`: +2. Look in the immediately outher class/interface scope for declarations of `with Monoid` in this case the class `AddingInts`: ```kotlin -instance class AddingInts given Monoid { +extension class AddingInts(with Monoid) { fun addInts(): Int = add(1, 2) } ``` @@ -280,7 +273,7 @@ This will compile because the responsibility of providing `Monoid` is passe 3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid`: ```kotlin import intext.IntMonoid -instance class AddingInts { +extension class AddingInts { fun addInts(): Int = add(1, 2) } ``` @@ -288,12 +281,10 @@ This will compile because the responsibility of providing `Monoid` is satis 4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. ```kotlin -instance class AddingInts { +extension class AddingInts { fun addInts(): Int = add(1, 2) } ``` -Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`.: - - +Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`. In such case the parameter value have to be explicitly defined. Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 From 0496c7340c9d59a841203e0382d8ff2f9e212b4e Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Wed, 11 Apr 2018 08:49:46 +0200 Subject: [PATCH 18/73] Fix lambda declaration --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 20547d679..8edb8706f 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -195,7 +195,7 @@ Here `F<_>` refers to a type constructor meaning a type that has a hole on it su A use of this declaration in a polymorphic function would look like: ```kotlin -fun , A, B> transform(fa: F, f: (A): B, with Functor): F = F.map(fa, f) +fun , A, B> transform(fa: F, f: (A) -> B, with Functor): F = 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<_> From da913fe179f84672e07265aac18d8f9eb2a229de Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Wed, 11 Apr 2018 08:56:15 +0200 Subject: [PATCH 19/73] Compile resolution rules #4 clarification --- proposals/type-classes.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 8edb8706f..e0741aeb3 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -257,7 +257,7 @@ The compiler may choose the following order for resolving the evidence that a `M This will compile because the responsibility of providing `Monoid` is passed into the callers of `addInts()`: ```kotlin -extension class AddingInts { +extension object AddingInts { fun addInts(with Monoid): Int = add(1, 2) } ``` @@ -273,7 +273,7 @@ This will compile because the responsibility of providing `Monoid` is passe 3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid`: ```kotlin import intext.IntMonoid -extension class AddingInts { +extension object AddingInts { fun addInts(): Int = add(1, 2) } ``` @@ -281,10 +281,16 @@ This will compile because the responsibility of providing `Monoid` is satis 4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. ```kotlin -extension class AddingInts { +extension object AddingInts { fun addInts(): Int = add(1, 2) } ``` -Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`. In such case the parameter value have to be explicitly defined. +Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`. +In such case the extension instance have to be explicitly defined. +```kotlin +extension object AddingInts { + fun addInts(): Int = with(intext.IntMonoid) { add(1, 2) } +} +``` Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 From 00d6acafe8005b251ffd39e0e451eb089a70ad6c Mon Sep 17 00:00:00 2001 From: Claire Neveu Date: Mon, 25 Jun 2018 17:31:41 -0400 Subject: [PATCH 20/73] Update type class KEEP --- proposals/type-classes.md | 230 ++++++++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 85 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index e0741aeb3..7724a915f 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -1,4 +1,4 @@ -# Type classes +# Type Classes * **Type**: Design proposal * **Author**: Raul Raja @@ -8,19 +8,18 @@ ## Summary The goal of this proposal is to enable `type classes` 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. -Furthermore introduction of type classes improves usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. + +Type classes are a form of interface that provide a greater degree of polymorphism than classical interfaces. Typeclasses can also improve code-reuse in contrast to classical interfaces if done correctly. + +Introduction of type classes improves usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. ## Motivation -* Support Type class evidence compile time verification. +* Support type class 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 Type class pattern. * Enable better compile reified generics without the need for explicit inlining. -* Enable definition of polymorphic functions whose constrains can be verified at compile time in call sites. +* Enable definition of polymorphic functions whose constraints can be verified at compile time in call sites. ## Description @@ -39,65 +38,34 @@ In the implementation below we provide evidence that there is a `Monoid` in ```kotlin package intext -extension object IntMonoid : Monoid { +extension object : Monoid { fun Int.combine(b: Int): Int = this + b val empty: Int = 0 } ``` -``` -import intext.IntMonoid - -1.combine(2) // 3 -IntMonoid.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 -import intext.IntMonoid - -fun add(a: A, b: A, with Monoid): A = a.combine(b) -add(1, 1) // compiles -add("a", "b") // does not compile: No `String: Monoid` instance defined in scope -``` - -## Compile Time Dependency Injection - -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 -extension interface Context { - fun A.config(): Config -} -``` - +Type class implementations can be given a name for Java interop. ```kotlin -package prod +package intext -extension object ProdContext: Context { - fun Service.config(): Config = ProdConfig +extension object IntMonoid : Monoid { + fun Int.combine(b: Int): Int = this + b + val empty: Int = 0 } ``` ```kotlin -package test -extension object TestContext: Context { - fun Service.config(): Config = TestConfig -} +1.combine(2) // 3 +Monoid.empty() // 0 ``` -```kotlin -package prod - -service.config() // ProdConfig -``` +Because of this constraint where we are stating that there is a `Monoid` constraint for a given type `A` we can also encode polymorphic definitions based on those constraints: ```kotlin -package test - -service.config() // TestConfig +fun add(a: A, b: A, with Monoid): A = a.combine(b) +add(1, 1) // compiles +add("a", "b") // does not compile: No `String: Monoid` instance defined in scope ``` ## Overcoming `inline` + `reified` limitations @@ -140,7 +108,7 @@ extension class Foo { ## Composition and chain of evidences -Type class instances and declarations can encode further constrains in their generic args so they can be composed nicely: +Type class instances and declarations can encode further constraints in their generic args so they can be composed nicely: ```kotlin package optionext @@ -164,9 +132,6 @@ extension class OptionMonoid(with Monoid): Monoid> { The above instance declares a `Monoid>` as long as there is a `Monoid` in scope. ```kotlin -import optionext.OptionMonoid -import intext.IntMonoid - Option(1).combine(Option(1)) // Option(2) Option("a").combine(Option("b")) // does not compile. Found `Monoid>` instance providing `combine` but no `Monoid` instance was in scope ``` @@ -184,7 +149,7 @@ extension interface FunctionK, G<_>> { fun invoke(fa: F): G } -extension object Option2List : FunctionK { +extension object : FunctionK { fun invoke(fa: Option): List = fa.fold({ emptyList() }, { listOf(it) }) } @@ -237,60 +202,155 @@ fun addInts(a: Int, b: Int, with Monoid): Int = add(a, b) fun addInts(a: Int, b: Int): Int = with(intext.IntMonoid) { add(a, b) } ``` -## Compile resolution rules +## Type Class Instance Rules -When the compiler finds a call site invoking a function that has type class instances constrains declared with `with` as in the example below: +Classical interfaces only allow the implementation of interfaces to occur when a type is defined. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxinng this rule it is important to preserve the coherency we take for granted with classical interfaces. -Declaration: +For those reasons type class instances must be declared in one of two places: + +1. In the same file as the type class definition (interface-side implementation) +2. In the same file as the type being implemented (type-side implementation) + +All other instances are orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. + +Additionally a type class implementation must not conflict with any other already defined type class implementations; for the purposes of checking this we use the normal resolution rules. + +### Interface-Side Implementations + +This definition site is simple to implement and requires to rules except that the instances occurs in the same package. E.g. the following implementation is allowed ```kotlin -fun add(a: A, b: A, with Monoid): A = a.combine(b) +package foo.collections + +extension interface Monoid { + ... +} ``` -Call site: + ```kotlin -extension class AddingInts { - fun addInts(): Int = add(1, 2) +package foo.collections + +extension object : Monoid { + ... } ``` -The compiler may choose the following order for resolving the evidence that a `Monoid` exists in scope. -1. Look in the most immediate scope for declarations of `with Monoid` in this case the function `addInts` +### Type-Side Implementations + +This definition site poses additional complications when you consider multi-parameter typeclasses. -This will compile because the responsibility of providing `Monoid` is passed into the callers of `addInts()`: ```kotlin -extension object AddingInts { - fun addInts(with Monoid): Int = add(1, 2) +package foo.collections + +extension interface Isomorphism { + ... } ``` -2. Look in the immediately outher class/interface scope for declarations of `with Monoid` in this case the class `AddingInts`: ```kotlin -extension class AddingInts(with Monoid) { - fun addInts(): Int = add(1, 2) +package data.foo + +data class Foo(...) +extension class : Isomorphism { + ... } ``` -This will compile because the responsibility of providing `Monoid` is passed unto the callers of `AddingInts()` -3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid`: ```kotlin -import intext.IntMonoid -extension object AddingInts { - fun addInts(): Int = add(1, 2) +package data.bar + +data class Bar(...) +extension class : Isomorphism { + ... } ``` -This will compile because the responsibility of providing `Monoid` is satisfied by `import intext.IntMonoid` -4. Fail to compile if neither outer scopes nor explicit imports fail to provide evidence that there is a constrain satisfied by an instance in scope. +The above instances are each defined alongside their respective type definitions and yet they clearly conflict with each other. We will also run into quandaries once we consider generic types. We can crib some prior art from Rust1 to help us out here. + +To determine whether a typeclass definition is a valid type-side implementation we perform the following check: + +1. A "local type" is any type (but not typealias) defined in the current file (e.g. everything defined in `data.bar` if we're evaluating `data.bar`). +2. A generic type parameter is "covered" by a type if it occurs within that type, e.g. `MyType` covers `T` in `MyType` but not `Pair`. +3. Write out the parameters to the type class in order. +4. The parameters must include a type defined in this file. +5. Any generic type parameters must occur after the first instance of a local type or be covered by a local type. + +If a type class implementation meets these rules it is a valid type-side implementation. + + +## Compile Resolution Rules + +When the compiler finds a call site invoking a function that has type class instances constraints declared with `with` as in the example below: + +Declaration: +```kotlin +fun add(a: A, b: A, with Monoid): A = a.combine(b) +``` +Call site: ```kotlin -extension object AddingInts { - fun addInts(): Int = add(1, 2) +fun addInts(): Int = add(1, 2) +``` + +1. The compiler first looks at the file the interface is defined in. If it finds exactly one implementation it uses that instance. +2. If it fails to find an implementation in the interface's file, it then looks at the files of the implemented types. For each type class parameter check the file it was defined in. If exactly one implementation is found use that instance. +3. If no matching implementation is found in either of these places fail to compile. +4. If more than one matching implementation is found, fail to compile and indicate that there or conflicting instances. + +Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 + +## Appendix A: Orphan Implementations + +Orphan implementations are a subject of controversy. Combining two libraries, one defining a data type, the other defining an interface, is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. + +Orphan implementations are the reason that type classes have often been described as "anti-modular" as the most common way of dealing with them is through global coherency checks. This is necessary to ensure that two libraries have not defined incompatible implementations of a type class interface. + +Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans it is useful to consider how they could be added in the future. + +Ideally we want to ban orphan implementations in libraries but not in executables; this allows a programmer to manually deal with coherence in their own code but prevents situations where adding a new library breaks code. + +### Package-based Approach to Orphans + +A simple way to allow orphan implementations is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages it is posible to do the following. + +```kotlin +// In some library foo +package foo.collections + +extension class Monoid { + ... } ``` -Fails to compile lacking evidence that you can invoke `add(1,2)` since `add` is a polymorphic function that requires a `Monoid` inferred by `1` and `2` being of type `Int`. -In such case the extension instance have to be explicitly defined. + ```kotlin -extension object AddingInts { - fun addInts(): Int = with(intext.IntMonoid) { add(1, 2) } +// In some application that uses the foo library +package foo.collections + +extension object : Monoid { + ... } ``` -Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 +This approach would not forbid orphan implementations in libraries but it would highly discourage them from providing them since it would involve writing code in the package namespace of another library. + +### Internal Modifier-based Approach to Orphans + +An alternate approach is to require that orphan implementations be marked `internal`. The full rules would be as follows: + +1. All orphan implementations must be marked `internal` +2. All code which closes over an internal implementations must be marked internal. Code closes over a type class instance if it contains a static reference to such an implementation. +3. Internal implementations defined in the same module are in scope for the current module. +4. Internal implementations defined in other modules are not valid for type class resolution. + +This approach works well but it has a few problems. + +1. It forces applications that use orphan implementations to mark all their code as internal, which is a lot of syntactic noise. +2. It complicates the compiler's resolution mechanism since it's not as easy to enumerate definition sites. + +The first problem can actually leads us to a better solution. + +### Java 9 Module-based Approach to Orphans + +Currently Kotlin does not make use of Java 9 modules but it is easy to see how they could eventually replace Kotlin's `internal` modifier. The rules for this approach would be the same as the `internal`-based approach; code which uses orphans is not allowed to be exported. + +## Footnotes + +1. [Little Orphan Impls](http://smallcultfollowing.com/babysteps/blog/2015/01/14/little-orphan-impls/) From f495563f8fb46d894302338342ce0f4b5c6207ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Thu, 28 Jun 2018 17:06:31 +0200 Subject: [PATCH 21/73] Update type-classes.md --- proposals/type-classes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 7724a915f..4154aa976 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -2,6 +2,7 @@ * **Type**: Design proposal * **Author**: Raul Raja +* **Contributors**: Francesco Vasco, Claire Neveu * **Status**: New * **Prototype**: - From d70f40c900535db275f7fd20cea183b047d72353 Mon Sep 17 00:00:00 2001 From: Claire Neveu Date: Thu, 28 Jun 2018 11:35:33 -0400 Subject: [PATCH 22/73] Remove out-of-date section. --- proposals/type-classes.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 4154aa976..a14d291a4 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -179,30 +179,6 @@ fun add(a: A, b: A, with Monoid): A = a.combine(b) //function position us fun add(a: A, b: A, with M: Monoid): A = a.combine(b) //function position argument "M" ``` -## Compiler Changes - -- The type checker will declare the below definition as valid since the `with` clause provides evidence that call sites won't be able to compile calls to this function unless a `Monoid` is in scope. -```kotlin -fun add(a: A, b: A, with Monoid): A = a.combine(b) //compiles -``` -- The type checker will declare the below definition as invalid since there is no `Monoid` in scope. -```kotlin -add(1, 2) -``` -- The type checker will declare the below definition as valid since there is a `Monoid` in scope. -```kotlin -import intext.IntMonoid -add(1, 2) -``` -- The type checker will declare the below definition as valid since there is a `Monoid` in scope. -```kotlin -fun addInts(a: Int, b: Int, with Monoid): Int = add(a, b) -``` -- The type checker will declare the below definition as valid since there is a `with` block around the concrete `IntMonoid` in scope. -```kotlin -fun addInts(a: Int, b: Int): Int = with(intext.IntMonoid) { add(a, b) } -``` - ## Type Class Instance Rules Classical interfaces only allow the implementation of interfaces to occur when a type is defined. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxinng this rule it is important to preserve the coherency we take for granted with classical interfaces. From 4671389152b449abe1851d23ac3fcb8fd270a85c Mon Sep 17 00:00:00 2001 From: Francesco Vasco Date: Fri, 29 Jun 2018 13:05:12 +0200 Subject: [PATCH 23/73] anonymous parameter clarification (#8) --- proposals/type-classes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index a14d291a4..242381a26 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -172,11 +172,11 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc - Add `with` to require instances evidences in both function and class/object declarations as demonstrated by previous and below examples: ```kotlin -extension class OptionMonoid(with Monoid) : Monoid> //class position using argument "Monoid" -extension class OptionMonoid(with M: Monoid) : Monoid> //class position using argument "M" +extension class OptionMonoid(with M: Monoid) : Monoid> // class position using parameter "M" +extension class OptionMonoid(with Monoid) : Monoid> // class position using anonymous `Monoid` parameter -fun add(a: A, b: A, with Monoid): A = a.combine(b) //function position using argument "Monoid" -fun add(a: A, b: A, with M: Monoid): A = a.combine(b) //function position argument "M" +fun add(a: A, b: A, with M: Monoid): A = a.combine(b) // function position using parameter "M" +fun add(a: A, b: A, with Monoid): A = a.combine(b) // function position using anonymous `Monoid` parameter ``` ## Type Class Instance Rules From 84d16ce81bf63bbac74d5e18783c0b655599b747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ruiz-L=C3=B3pez?= Date: Wed, 14 Nov 2018 12:49:24 +0100 Subject: [PATCH 24/73] Update proposal based on the initial implementation (#10) * Update proposal based on the initial implementation * Add suggested changes from comments --- proposals/type-classes.md | 52 +++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 242381a26..55476e5cd 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -2,9 +2,9 @@ * **Type**: Design proposal * **Author**: Raul Raja -* **Contributors**: Francesco Vasco, Claire Neveu +* **Contributors**: Francesco Vasco, Claire Neveu, Tomás Ruiz López * **Status**: New -* **Prototype**: - +* **Prototype**: [initial implementation](https://github.com/arrow-kt/kotlin/pull/6) ## Summary @@ -27,7 +27,7 @@ Introduction of type classes improves usages for `reified` generic functions wit We propose to use the existing `interface` semantics allowing for generic definition of type classes and their instances with the same style interfaces are defined ```kotlin -extension interface Monoid { +interface Monoid { fun A.combine(b: A): A val empty: A } @@ -170,7 +170,10 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc ## Language Changes -- Add `with` to require instances evidences in both function and class/object declarations as demonstrated by previous and below examples: +- Add `with` to require instances evidences in both function and class/object declarations +- Add `extension` to provide instance evidences for a given type class + +As demonstrated by previous and below examples: ```kotlin extension class OptionMonoid(with M: Monoid) : Monoid> // class position using parameter "M" extension class OptionMonoid(with Monoid) : Monoid> // class position using anonymous `Monoid` parameter @@ -183,10 +186,12 @@ fun add(a: A, b: A, with Monoid): A = a.combine(b) // function position u Classical interfaces only allow the implementation of interfaces to occur when a type is defined. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxinng this rule it is important to preserve the coherency we take for granted with classical interfaces. -For those reasons type class instances must be declared in one of two places: +For those reasons type class instances must be declared in one of these places: -1. In the same file as the type class definition (interface-side implementation) -2. In the same file as the type being implemented (type-side implementation) +1. In the companion object of the type class (interface-side implementation). +2. In the companion object of the type implementing the type class (type-side implementation). +3. In a subpackage of the package where the type class is defined. +4. In a subpackage of the package where the type implementing the type class is defined. All other instances are orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. @@ -198,13 +203,16 @@ This definition site is simple to implement and requires to rules except that th ```kotlin package foo.collections -extension interface Monoid { +interface Monoid { ... + companion object { + extension object IntMonoid : Monoid { ... } + } } ``` ```kotlin -package foo.collections +package foo.collections.instances extension object : Monoid { ... @@ -218,13 +226,13 @@ This definition site poses additional complications when you consider multi-para ```kotlin package foo.collections -extension interface Isomorphism { +interface Isomorphism { ... } ``` ```kotlin -package data.foo +package data.collections.foo data class Foo(...) extension class : Isomorphism { @@ -233,7 +241,7 @@ extension class : Isomorphism { ``` ```kotlin -package data.bar +package data.collections.bar data class Bar(...) extension class : Isomorphism { @@ -245,7 +253,7 @@ The above instances are each defined alongside their respective type definitions To determine whether a typeclass definition is a valid type-side implementation we perform the following check: -1. A "local type" is any type (but not typealias) defined in the current file (e.g. everything defined in `data.bar` if we're evaluating `data.bar`). +1. A "local type" is any type (but not typealias) defined in the current file (e.g. everything defined in `data.collections.bar` if we're evaluating `data.collections.bar`). 2. A generic type parameter is "covered" by a type if it occurs within that type, e.g. `MyType` covers `T` in `MyType` but not `Pair`. 3. Write out the parameters to the type class in order. 4. The parameters must include a type defined in this file. @@ -267,12 +275,24 @@ Call site: fun addInts(): Int = add(1, 2) ``` -1. The compiler first looks at the file the interface is defined in. If it finds exactly one implementation it uses that instance. -2. If it fails to find an implementation in the interface's file, it then looks at the files of the implemented types. For each type class parameter check the file it was defined in. If exactly one implementation is found use that instance. +1. The compiler first looks at the function context where the invocation is happening. If a function argument matches the required instance for a typeclass, it uses that instance; e.g.: + +```kotlin +fun duplicate(a : A, with M: Monoid): A = a.combine(a) +``` + +The invocation `a.combine(a)` requires a `Monoid` and since one is passed as an argument to `duplicate`, it uses that one. + +2. In case it fails, it inspects the following places, sequentially, until it is able to find a valid unique instance for the typeclass: + a. The current package (where the invocation is taking place), as long as the `extension` is `internal`. + b. The companion object of the type parameter(s) in the type class (e.g. in `Monoid`, it looks into `A`'s companion object). + c. The companion object of the type class. + d. The subpackages of the package where the type parameter(s) in the type class is defined. + e. The subpackages of the package where the type class is defined. 3. If no matching implementation is found in either of these places fail to compile. 4. If more than one matching implementation is found, fail to compile and indicate that there or conflicting instances. -Some of these examples where originally proposed by Roman Elizarov and the Kategory contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 +Some of these examples were originally proposed by Roman Elizarov and the Arrow contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 ## Appendix A: Orphan Implementations From b632229b957d57267fae62d1ba809a0dbb691008 Mon Sep 17 00:00:00 2001 From: Paco Date: Wed, 14 Nov 2018 06:51:11 -0500 Subject: [PATCH 25/73] Fix misuse of encoding (#9) * Fix misuse of encoding * Fix typos --- proposals/type-classes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 55476e5cd..72719060f 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -161,7 +161,7 @@ Here `F<_>` refers to a type constructor meaning a type that has a hole on it su A use of this declaration in a polymorphic function would look like: ```kotlin -fun , A, B> transform(fa: F, f: (A) -> B, with Functor): F = F.map(fa, f) +fun , A, B> transform(fa: F, f: (A) -> B, with Functor): F = fa.map(f) transform(Option(1), { it + 1 }) // Option(2) transform("", { it + "b" }) // Does not compile: `String` is not type constructor with shape F<_> @@ -292,7 +292,8 @@ The invocation `a.combine(a)` requires a `Monoid` and since one is passed as 3. If no matching implementation is found in either of these places fail to compile. 4. If more than one matching implementation is found, fail to compile and indicate that there or conflicting instances. -Some of these examples were originally proposed by Roman Elizarov and the Arrow contributors where these features where originally discussed https://github.com/Kotlin/KEEP/pull/87 +Some of these examples were originally proposed by Roman Elizarov and the Arrow contributors where these features were originally discussed https://github.com/Kotlin/KEEP/pull/87 + ## Appendix A: Orphan Implementations From 2a8b95a5d067795af0f953e71932bf53daa2d2b5 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Nov 2018 20:27:45 +0000 Subject: [PATCH 26/73] Linguistic improvements within type-classes.md (#11) * Linguistic improvements within type-classes.md I've tried to improve the use of language within the proposal without making any changes to the proposal itself. I'd encourage that these changes be carefully reviewed individually, since some are just typographical / grammatical corrections whereas others are minor stylistic changes that you might disagree with. In particular, different conventions are adopted throughout the document, so I've tried to standardise the conventions used. For example, the headers are a mix of title case ("Type-Side Implementations") and sentence case ("Composition and chain of evidences"), so I've changed them all to sentence case. I've also made all items within lists end with full-stops, except the number list in "Type Class Instance Rules". "Type classes" has occasionally been spelt as "typeclasses", so I've fixed all occurrences to "type classes". I've rewritten some sentences completely in ways that I find are clearer, but you might not agree. I've made some minor formatting changes, such as standardising comments within code snippets to have a single space after the `//`. * Further linguistic and formatting improvements Improves formatting and the use of language within new additions to the proposal. Re-applies changes that were originally made but lost following prior PR merges. --- proposals/type-classes.md | 140 ++++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 67 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 72719060f..91f0525e1 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -8,23 +8,23 @@ ## Summary -The goal of this proposal is to enable `type classes` and lightweight `Higher Kinded Types` in Kotlin to enable ad-hoc polymorphism and better extension syntax. +The goal of this proposal is to enable `type classes` and lightweight `higher kinded types` in Kotlin to enable ad-hoc polymorphism and better extension syntax. -Type classes are a form of interface that provide a greater degree of polymorphism than classical interfaces. Typeclasses can also improve code-reuse in contrast to classical interfaces if done correctly. +Type classes are a form of interface that provide a greater degree of polymorphism than classical interfaces. Type classes can also improve code reuse in contrast to classical interfaces if done correctly. -Introduction of type classes improves usages for `reified` generic functions with a more robust approach that does not require those to be `inline` or `reified`. +The introduction of type classes allow the substitution of `reified` generic functions with a more robust approach that does not require functions to be declared `inline` or `reified`. ## Motivation -* Support type class evidence compile time verification. +* Support type class evidence compile-time verification. * Support a broader range of Typed FP patterns. -* Enable multiple extension functions groups for type declarations. -* Enable better compile reified generics without the need for explicit inlining. +* Enable multiple extension function groups for type declarations. +* Enable the use of reified generics without the need for explicit inlining. * Enable definition of polymorphic functions whose constraints can be verified at compile time in call sites. ## Description -We propose to use the existing `interface` semantics allowing for generic definition of type classes and their instances with the same style interfaces are defined +We propose to use the existing `interface` semantics, allowing for generic definition of type classes and their instances in the same style interfaces are defined. ```kotlin interface Monoid { @@ -33,8 +33,8 @@ interface Monoid { } ``` -The above declaration can serve as target for implementations for any arbitrary data type. -In the implementation below we provide evidence that there is a `Monoid` instance that enables `combine` and `empty` on `Int` +The above declaration can serve as a target for implementations of any arbitrary data type. +In the implementation below we provide evidence that there is a `Monoid` instance, enabling `combine` and `empty` on `Int`. ```kotlin package intext @@ -46,6 +46,7 @@ extension object : Monoid { ``` Type class implementations can be given a name for Java interop. + ```kotlin package intext @@ -61,7 +62,7 @@ extension object IntMonoid : Monoid { Monoid.empty() // 0 ``` -Because of this constraint where we are stating that there is a `Monoid` constraint for a given type `A` we can also encode polymorphic definitions based on those constraints: +Type classes can be used as constraints on type parameters within polymorphic functions. In the example below, we constrain `A` to being a type for which evidence exists that it can be treated as a `Monoid`. ```kotlin fun add(a: A, b: A, with Monoid): A = a.combine(b) @@ -71,7 +72,7 @@ add("a", "b") // does not compile: No `String: Monoid` instance defined in scope ## Overcoming `inline` + `reified` limitations -Type classes allow us to workaround `inline` `reified` generics and their limitations and express those as type classes instead: +Type classes allow us to work around `inline` `reified` generics and their limitations and express those as type classes instead: ```kotlin extension interface Reified { @@ -91,25 +92,25 @@ can be replaced with: fun fooTC(with Reified): Klass { .... A.selfClass ... } ``` -This allows us to obtain generics info without the need to declare the functions `inline` or `reified` overcoming the current limitations of inline reified functions that can't be invoked unless made concrete from non reified contexts. +This allows us to obtain generics info without the need to declare functions as `inline` or `reified`, overcoming the current limitation of inline reified functions in that they can't be invoked unless made concrete from non-reified contexts. -Not this does not remove the need to use `inline reified` where one tries to instrospect generic type information at runtime with reflection. This particular case is only relevant for those cases where you know the types you want `Reified` ahead of time and you need to access to their class value. +Note that this does not remove the need to use `inline reified` where one tries to inspect generic type information at runtime through reflection. This particular case is only relevant for those cases where you know the types you want `reified` ahead of time and you need to access their class value. ```kotlin extension class Foo { - val someKlazz = foo() //won't compile because class disallow reified type args. + val someKlazz = foo() // won't compile because class disallow reified type args } ``` ```kotlin extension class Foo { - val someKlazz = fooTC() //works and does not requires to be inside an `inline reified` context. + val someKlazz = fooTC() // works and does not require to be inside an `inline reified` context } ``` ## Composition and chain of evidences -Type class instances and declarations can encode further constraints in their generic args so they can be composed nicely: +Type class instances and declarations can encode further constraints on their type parameters so that they can be composed nicely: ```kotlin package optionext @@ -126,7 +127,7 @@ extension class OptionMonoid(with Monoid): Monoid> { } is None -> this } - + } ``` @@ -137,11 +138,11 @@ Option(1).combine(Option(1)) // Option(2) Option("a").combine(Option("b")) // does not compile. Found `Monoid>` instance providing `combine` but no `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. +We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support type classes such as Scala where this is done via implicits. -## Typeclasses over type constructors +## Type classes 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 +We recommend if this proposal is accepted that a lightweight version of higher kinds support is included to unveil the true power of type classes through the extensions mechanisms. A syntax that would allow for higher kinds in these definitions may look like this: @@ -154,9 +155,9 @@ extension object : FunctionK { fun invoke(fa: Option): List = fa.fold({ emptyList() }, { listOf(it) }) } -``` +``` -Here `F<_>` refers to a type constructor meaning a type that has a hole on it such as `Option`, `List`, etc. +Here `F<_>` refers to a type constructor, meaning a type that has a hole within it such as `Option`, `List`, etc. A use of this declaration in a polymorphic function would look like: @@ -168,12 +169,13 @@ transform("", { it + "b" }) // Does not compile: `String` is not type constructo transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instance defined in scope. ``` -## Language Changes +## Language changes -- Add `with` to require instances evidences in both function and class/object declarations -- Add `extension` to provide instance evidences for a given type class +* Add `with` to require evidence of type class instances in both function and class/object declarations. +* Add `extension` to provide instance evidence for a given type class. + +Usage of these language changes are demonstrated by the previous and below examples: -As demonstrated by previous and below examples: ```kotlin extension class OptionMonoid(with M: Monoid) : Monoid> // class position using parameter "M" extension class OptionMonoid(with Monoid) : Monoid> // class position using anonymous `Monoid` parameter @@ -182,9 +184,9 @@ fun add(a: A, b: A, with M: Monoid): A = a.combine(b) // function positio fun add(a: A, b: A, with Monoid): A = a.combine(b) // function position using anonymous `Monoid` parameter ``` -## Type Class Instance Rules +## Type class instance rules -Classical interfaces only allow the implementation of interfaces to occur when a type is defined. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxinng this rule it is important to preserve the coherency we take for granted with classical interfaces. +Classical interfaces only permit their implementation at the site of a type definition. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxing this rule it is important to preserve the coherency we take for granted with classical interfaces. For those reasons type class instances must be declared in one of these places: @@ -195,11 +197,12 @@ For those reasons type class instances must be declared in one of these places: All other instances are orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. -Additionally a type class implementation must not conflict with any other already defined type class implementations; for the purposes of checking this we use the normal resolution rules. +Additionally, a type class instance must not conflict with any other pre-existing type class instances; for the purposes of checking this we use the normal resolution rules. + +### Interface-side implementations -### Interface-Side Implementations +This definition site is simple to implement and requires no rules except that the instances occur in the same package. For example, the following implementation is allowed: -This definition site is simple to implement and requires to rules except that the instances occurs in the same package. E.g. the following implementation is allowed ```kotlin package foo.collections @@ -219,9 +222,9 @@ extension object : Monoid { } ``` -### Type-Side Implementations +### Type-side implementations -This definition site poses additional complications when you consider multi-parameter typeclasses. +This definition site poses additional complications when you consider multi-parameter type classes. ```kotlin package foo.collections @@ -251,63 +254,66 @@ extension class : Isomorphism { The above instances are each defined alongside their respective type definitions and yet they clearly conflict with each other. We will also run into quandaries once we consider generic types. We can crib some prior art from Rust1 to help us out here. -To determine whether a typeclass definition is a valid type-side implementation we perform the following check: +To determine whether a type class definition is a valid type-side implementation we perform the following check: -1. A "local type" is any type (but not typealias) defined in the current file (e.g. everything defined in `data.collections.bar` if we're evaluating `data.collections.bar`). -2. A generic type parameter is "covered" by a type if it occurs within that type, e.g. `MyType` covers `T` in `MyType` but not `Pair`. +1. A "local type" is any type (but not type alias) defined in the current file (e.g. everything defined in `data.collections.bar` if we're evaluating `data.collections.bar`). +2. A generic type parameter is "covered" by a type if it occurs within that type (e.g. `MyType` covers `T` in `MyType` but not `Pair`). 3. Write out the parameters to the type class in order. 4. The parameters must include a type defined in this file. 5. Any generic type parameters must occur after the first instance of a local type or be covered by a local type. If a type class implementation meets these rules it is a valid type-side implementation. - -## Compile Resolution Rules +## Compile resolution rules -When the compiler finds a call site invoking a function that has type class instances constraints declared with `with` as in the example below: +When the compiler finds a call site of a function that has type class instance constraints declared with `with`, as in the example below: + +Declaration: -Declaration: ```kotlin fun add(a: A, b: A, with Monoid): A = a.combine(b) ``` + Call site: + ```kotlin fun addInts(): Int = add(1, 2) ``` -1. The compiler first looks at the function context where the invocation is happening. If a function argument matches the required instance for a typeclass, it uses that instance; e.g.: +1. The compiler first looks at the function context where the invocation is happening. If a function argument matches the required instance for a type class, it uses that instance; e.g.: -```kotlin -fun duplicate(a : A, with M: Monoid): A = a.combine(a) -``` + ```kotlin + fun duplicate(a : A, with M: Monoid): A = a.combine(a) + ``` -The invocation `a.combine(a)` requires a `Monoid` and since one is passed as an argument to `duplicate`, it uses that one. + The invocation `a.combine(a)` requires a `Monoid` and since one is passed as an argument to `duplicate`, it uses that one. -2. In case it fails, it inspects the following places, sequentially, until it is able to find a valid unique instance for the typeclass: - a. The current package (where the invocation is taking place), as long as the `extension` is `internal`. - b. The companion object of the type parameter(s) in the type class (e.g. in `Monoid`, it looks into `A`'s companion object). - c. The companion object of the type class. - d. The subpackages of the package where the type parameter(s) in the type class is defined. - e. The subpackages of the package where the type class is defined. -3. If no matching implementation is found in either of these places fail to compile. -4. If more than one matching implementation is found, fail to compile and indicate that there or conflicting instances. +2. In case it fails, it inspects the following places, sequentially, until it is able to find a valid unique instance for the type class: -Some of these examples were originally proposed by Roman Elizarov and the Arrow contributors where these features were originally discussed https://github.com/Kotlin/KEEP/pull/87 + * The current package (where the invocation is taking place), as long as the `extension` is `internal`. + * The companion object of the type parameter(s) in the type class (e.g. in `Monoid`, it looks into `A`'s companion object). + * The companion object of the type class. + * The subpackages of the package where the type parameter(s) in the type class is defined. + * The subpackages of the package where the type class is defined. + +3. If no matching implementation is found in either of these places then the code fails to compile. +4. If more than one matching implementation is found, then the code fails to compile and the compiler indicates that there are conflicting instances. +Some of these examples were proposed by Roman Elizarov and the Arrow contributors where these features were originally discussed: https://github.com/Kotlin/KEEP/pull/87 -## Appendix A: Orphan Implementations +## Appendix A: Orphan implementations -Orphan implementations are a subject of controversy. Combining two libraries, one defining a data type, the other defining an interface, is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. +Orphan implementations are a subject of controversy. Combining two libraries - one defining a data type, the other defining an interface - is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. -Orphan implementations are the reason that type classes have often been described as "anti-modular" as the most common way of dealing with them is through global coherency checks. This is necessary to ensure that two libraries have not defined incompatible implementations of a type class interface. +Orphan implementations are the reason that type classes have often been described as "anti-modular", as the most common way of dealing with them is through global coherence checks. This is necessary to ensure that two libraries have not defined incompatible implementations of a type class interface. -Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans it is useful to consider how they could be added in the future. +Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans then it's useful to consider how they could be added in the future. Ideally we want to ban orphan implementations in libraries but not in executables; this allows a programmer to manually deal with coherence in their own code but prevents situations where adding a new library breaks code. -### Package-based Approach to Orphans +### Package-based approach to orphans -A simple way to allow orphan implementations is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages it is posible to do the following. +A simple way to allow orphan implementations is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages, it is possible to do the following. ```kotlin // In some library foo @@ -327,14 +333,14 @@ extension object : Monoid { } ``` -This approach would not forbid orphan implementations in libraries but it would highly discourage them from providing them since it would involve writing code in the package namespace of another library. +This approach would not forbid orphan implementations in libraries but it would highly discourage libraries from providing them, as this would involve writing code in the package namespace of another library. -### Internal Modifier-based Approach to Orphans +### Internal modifier-based approach to orphans An alternate approach is to require that orphan implementations be marked `internal`. The full rules would be as follows: -1. All orphan implementations must be marked `internal` -2. All code which closes over an internal implementations must be marked internal. Code closes over a type class instance if it contains a static reference to such an implementation. +1. All orphan implementations must be marked `internal`. +2. All code which closes over an internal implementation must be marked internal. Code closes over a type class instance if it contains a static reference to such an implementation. 3. Internal implementations defined in the same module are in scope for the current module. 4. Internal implementations defined in other modules are not valid for type class resolution. @@ -343,11 +349,11 @@ This approach works well but it has a few problems. 1. It forces applications that use orphan implementations to mark all their code as internal, which is a lot of syntactic noise. 2. It complicates the compiler's resolution mechanism since it's not as easy to enumerate definition sites. -The first problem can actually leads us to a better solution. +The first problem actually leads us to a better solution. -### Java 9 Module-based Approach to Orphans +### Java 9 module-based approach to orphans -Currently Kotlin does not make use of Java 9 modules but it is easy to see how they could eventually replace Kotlin's `internal` modifier. The rules for this approach would be the same as the `internal`-based approach; code which uses orphans is not allowed to be exported. +Kotlin does not currently make use of Java 9 modules but it is easy to see how they could eventually replace Kotlin's `internal` modifier. The rules for this approach would be the same as the `internal`-based approach; code which uses orphans is not allowed to be exported. ## Footnotes From 3b742403507762abb2a5e5b8cf0463a33335719d Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Wed, 10 Apr 2019 15:09:41 +0200 Subject: [PATCH 27/73] =?UTF-8?q?Fix=20typo:=20The=20intro=E2=80=A6=20allo?= =?UTF-8?q?w=20->=20The=20intro=E2=80=A6=20allows=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proposals/type-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 91f0525e1..51054baae 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -12,7 +12,7 @@ The goal of this proposal is to enable `type classes` and lightweight `higher ki Type classes are a form of interface that provide a greater degree of polymorphism than classical interfaces. Type classes can also improve code reuse in contrast to classical interfaces if done correctly. -The introduction of type classes allow the substitution of `reified` generic functions with a more robust approach that does not require functions to be declared `inline` or `reified`. +The introduction of type classes allows the substitution of `reified` generic functions with a more robust approach that does not require functions to be declared `inline` or `reified`. ## Motivation From 5f7b61cfc3a2825f107c28f37da069031cd88562 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 09:08:06 +0200 Subject: [PATCH 28/73] Updates contributors list. --- proposals/type-classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/type-classes.md b/proposals/type-classes.md index 51054baae..22e4c8af5 100644 --- a/proposals/type-classes.md +++ b/proposals/type-classes.md @@ -2,7 +2,7 @@ * **Type**: Design proposal * **Author**: Raul Raja -* **Contributors**: Francesco Vasco, Claire Neveu, Tomás Ruiz López +* **Contributors**: Tomás Ruiz López, Jorge Castillo, Francesco Vasco, Claire Neveu * **Status**: New * **Prototype**: [initial implementation](https://github.com/arrow-kt/kotlin/pull/6) @@ -295,7 +295,7 @@ fun addInts(): Int = add(1, 2) * The companion object of the type class. * The subpackages of the package where the type parameter(s) in the type class is defined. * The subpackages of the package where the type class is defined. - + 3. If no matching implementation is found in either of these places then the code fails to compile. 4. If more than one matching implementation is found, then the code fails to compile and the compiler indicates that there are conflicting instances. From ce47c2ed3c402724ba3a88e624f43a76a48b456d Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 09:09:51 +0200 Subject: [PATCH 29/73] Renames keep file. --- .../{type-classes.md => compile-time-dependency-resolution.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{type-classes.md => compile-time-dependency-resolution.md} (100%) diff --git a/proposals/type-classes.md b/proposals/compile-time-dependency-resolution.md similarity index 100% rename from proposals/type-classes.md rename to proposals/compile-time-dependency-resolution.md From 2417af28798639e73e3f30fd3491e545a34e0629 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 09:22:51 +0200 Subject: [PATCH 30/73] First pass on Summary and Motivation. --- proposals/compile-time-dependency-resolution.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 22e4c8af5..157168dfd 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -1,4 +1,4 @@ -# Type Classes +# Compile time dependency resolution * **Type**: Design proposal * **Author**: Raul Raja @@ -8,18 +8,14 @@ ## Summary -The goal of this proposal is to enable `type classes` and lightweight `higher kinded types` in Kotlin to enable ad-hoc polymorphism and better extension syntax. - -Type classes are a form of interface that provide a greater degree of polymorphism than classical interfaces. Type classes can also improve code reuse in contrast to classical interfaces if done correctly. - -The introduction of type classes allows the substitution of `reified` generic functions with a more robust approach that does not require functions to be declared `inline` or `reified`. +The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we'd want to enable extension contract interfaces to be defined as function or class arguments and enable compiler to automatically resolve and inject those instances that must be provided evidence for in one of a given set of scopes. In case of not having evidence of any of those required interfaces (program constraints), compiler would fail and provide proper error messages. ## Motivation -* Support type class evidence compile-time verification. -* Support a broader range of Typed FP patterns. +* Support extension evidence compile-time verification. +* Enable nested extension resolution. * Enable multiple extension function groups for type declarations. -* Enable the use of reified generics without the need for explicit inlining. +* Support compile-time verification of a program correctness given behavioral constraints are raised to the interface types. * Enable definition of polymorphic functions whose constraints can be verified at compile time in call sites. ## Description From 34d89c8ca0804802737d64687fae47083af2a64c Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 10:11:02 +0200 Subject: [PATCH 31/73] Description first pass. --- .../compile-time-dependency-resolution.md | 75 +++++++++++++------ 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 157168dfd..b88ffcae3 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -20,50 +20,81 @@ The goal of this proposal is to enable **compile time dependency resolution** th ## Description -We propose to use the existing `interface` semantics, allowing for generic definition of type classes and their instances in the same style interfaces are defined. +We propose to use the existing `interface` semantics, allowing for generic definition of behaviors and their instances in the same style interfaces are defined. ```kotlin -interface Monoid { - fun A.combine(b: A): A - val empty: A +package com.data + +interface Repository { + fun loadAll(): List + fun loadById(id: Int): A? } ``` -The above declaration can serve as a target for implementations of any arbitrary data type. -In the implementation below we provide evidence that there is a `Monoid` instance, enabling `combine` and `empty` on `Int`. +The above declaration can serve as a target for implementations for any arbitrary type passed for `A`. + +In the implementation below we provide evidence that there is a `Repository` extensions available in scope, enabling both methods defined for the given behavior to work over the `User` type. As you can see, we're enabling a new keyword here: `extension`. ```kotlin -package intext +package com.data.instances -extension object : Monoid { - fun Int.combine(b: Int): Int = this + b - val empty: Int = 0 +import com.data.Repository +import com.domain.User + +extension object UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } + + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } } ``` -Type class implementations can be given a name for Java interop. +You can also provide evidence of an extension using classes: ```kotlin -package intext +package com.data.instances + +import com.data.Repository +import com.domain.User -extension object IntMonoid : Monoid { - fun Int.combine(b: Int): Int = this + b - val empty: Int = 0 +extension class UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } + + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } } ``` -```kotlin +**Extensions are named** for now, mostly for supporting Java, but we'd be open to iterate that towards allowing definition through properties and anonymous classes. +We got the contract definition (interface) and the way to provide evidence of an extension, we'd just need to connect both things now. Interfaces can be used to define constraints of a function or a class. We the `with` keyword for that. -1.combine(2) // 3 -Monoid.empty() // 0 +```kotlin +fun fetchById(id: String, with repository: Repository): A? { + return loadById(id) // Repository syntax is automatically activated inside the function scope! +} ``` -Type classes can be used as constraints on type parameters within polymorphic functions. In the example below, we constrain `A` to being a type for which evidence exists that it can be treated as a `Monoid`. +As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. + +On the call site: ```kotlin -fun add(a: A, b: A, with Monoid): A = a.combine(b) -add(1, 1) // compiles -add("a", "b") // does not compile: No `String: Monoid` instance defined in scope +fetch("1182938") // compiles since we got evidence of a `Repository` in scope. +fetch("1239821") // does not compile: No `Repository` evidence defined in scope! ``` ## Overcoming `inline` + `reified` limitations From 97f772ffb1fce55d149453b092aa7d3844211f1d Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 10:17:53 +0200 Subject: [PATCH 32/73] Pass over inline reified section. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index b88ffcae3..101520790 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -99,7 +99,7 @@ fetch("1239821") // does not compile: No `Repository` evidence defin ## Overcoming `inline` + `reified` limitations -Type classes allow us to work around `inline` `reified` generics and their limitations and express those as type classes instead: +Extension interface contracts allow us to work around `inline` `reified` generics and their limitations and express those as part of the constraints instead: ```kotlin extension interface Reified { From c6835ea3d1a0d2d0cc3822fbaea77a072b9a5f88 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 10:42:15 +0200 Subject: [PATCH 33/73] Pass over composition and chain evidences section. --- .../compile-time-dependency-resolution.md | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 101520790..eebfc6c74 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -137,35 +137,38 @@ extension class Foo { ## Composition and chain of evidences -Type class instances and declarations can encode further constraints on their type parameters so that they can be composed nicely: +Interface declarations and extension evidences can encode further constraints on their type parameters so that they can be composed nicely: ```kotlin -package optionext - -extension class OptionMonoid(with Monoid): Monoid> { +package com.data.instances - val empty: Option = None +import com.data.Repository +import com.domain.User +import com.domain.Group - fun Option.combine(ob: Option): Option = - when (this) { - is Some -> when (ob) { - is Some -> Some(this.value.combine(b.value)) //works because there is evidence of a Monoid - is None -> ob - } - is None -> this +extension class GroupRepository(with val repoA: Repository) : Repository> { + override fun loadAll(): List> { + return listOf(Group(userRepository.loadAll())) } + override fun loadById(id: Int): Group? { + return Group(userRepository.loadById(id)) + } } ``` -The above instance declares a `Monoid>` as long as there is a `Monoid` in scope. +The above extension provides evidence of a `Repository>` as long as there is a `Repository` in scope. Call site would be like: ```kotlin -Option(1).combine(Option(1)) // Option(2) -Option("a").combine(Option("b")) // does not compile. Found `Monoid>` instance providing `combine` but no `Monoid` instance was in scope +fun fetchGroup(with repo: GroupRepository) = repo.loadAll() + +fun main() { + fetchGroup() // Succeeds! There's evidence of Repository> and Repository provided in scope. + fetchGroup() // Fails! There's evidence of Repository> but no evidence of `Repository` available. +} ``` -We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support type classes such as Scala where this is done via implicits. +We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support compile time dependency resolution such as Scala where this is done via implicits. ## Type classes over type constructors From 9b6c6fdff74d669f92c8e4455e8268a2092849d9 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:07:21 +0200 Subject: [PATCH 34/73] Pass over Language changes section. --- .../compile-time-dependency-resolution.md | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index eebfc6c74..605e0402b 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -201,17 +201,49 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc ## Language changes -* Add `with` to require evidence of type class instances in both function and class/object declarations. -* Add `extension` to provide instance evidence for a given type class. +* Add `with` to require evidence of extensions in both function and class/object declarations. +* Add `extension` to provide instance evidence for a given interface. Usage of these language changes are demonstrated by the previous and below examples: ```kotlin -extension class OptionMonoid(with M: Monoid) : Monoid> // class position using parameter "M" -extension class OptionMonoid(with Monoid) : Monoid> // class position using anonymous `Monoid` parameter +// Class constraint +extension class GroupRepository(with R: Repository) : Repository> { + /* ... */ +} + +// Function constraint +fun fetch(id: String, with R: Repository): A = loadById(id) // function position using parameter "R" + +// Extension evidence using an Object +extension object UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } -fun add(a: A, b: A, with M: Monoid): A = a.combine(b) // function position using parameter "M" -fun add(a: A, b: A, with Monoid): A = a.combine(b) // function position using anonymous `Monoid` parameter + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } +} + +// Extension evidence using a Class +extension class UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } + + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } +} ``` ## Type class instance rules From f4ba65a27b89b81ef8aae7df18cae29127817e6e Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:09:40 +0200 Subject: [PATCH 35/73] Polish Language changes a bit. --- .../compile-time-dependency-resolution.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 605e0402b..02957a68d 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -206,16 +206,23 @@ transform(listOf(1), { it + 1 }) // does not compile: No `Functor` instanc Usage of these language changes are demonstrated by the previous and below examples: +#### Class constraint + ```kotlin -// Class constraint extension class GroupRepository(with R: Repository) : Repository> { /* ... */ } +``` + +#### Function constraint -// Function constraint +```kotlin fun fetch(id: String, with R: Repository): A = loadById(id) // function position using parameter "R" +``` -// Extension evidence using an Object +#### Extension evidence using an Object + +```kotlin extension object UserRepository: Repository { override fun loadAll(): List { return listOf(User(25, "Bob")) @@ -229,8 +236,11 @@ extension object UserRepository: Repository { } } } +``` -// Extension evidence using a Class +#### Extension evidence using a Class + +```kotlin extension class UserRepository: Repository { override fun loadAll(): List { return listOf(User(25, "Bob")) From ae5c0a72efed6d88f4639a33973a293a3d80e8d6 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:16:04 +0200 Subject: [PATCH 36/73] Review resolution order. --- .../compile-time-dependency-resolution.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 02957a68d..66be0130a 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -256,20 +256,21 @@ extension class UserRepository: Repository { } ``` -## Type class instance rules +## Extension resolution order -Classical interfaces only permit their implementation at the site of a type definition. Type classes typically relax this rule and allow implementations outside of the type definition. When relaxing this rule it is important to preserve the coherency we take for granted with classical interfaces. +Classical interfaces only permit their implementation at the site of a type definition. Compiler extension resolution pattern typically relax this rule and **allow extension evidences be declared outside of the type definition**. When relaxing this rule it is important to preserve the coherency we take for granted with classical interfaces. -For those reasons type class instances must be declared in one of these places: +For those reasons constraint interfaces must be declared in one of the following places (in strict resolution order): -1. In the companion object of the type class (interface-side implementation). -2. In the companion object of the type implementing the type class (type-side implementation). -3. In a subpackage of the package where the type class is defined. -4. In a subpackage of the package where the type implementing the type class is defined. +1. Scope of the caller function. +2. Companion object for the target type (User). +3. Companion object for the constraint interface we're looking for (Repository). +4. Subpackages of the package where the target type (User) to resolve is defined. +5. Subpackages of the package where the constraint interface (Repository) is defined. -All other instances are orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. +All other instances are considered orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. -Additionally, a type class instance must not conflict with any other pre-existing type class instances; for the purposes of checking this we use the normal resolution rules. +Additionally, a constraint extension must not conflict with any other pre-existing extension for the same constraint interface; for the purposes of checking this we use the normal resolution rules. That's what we refer as compiler "coherence". ### Interface-side implementations From 8c0737a1ada134dbdcf603c6ffb32bf339be029b Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:24:53 +0200 Subject: [PATCH 37/73] Remove type constructors section. --- .../compile-time-dependency-resolution.md | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 66be0130a..60dec1639 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -170,35 +170,6 @@ fun main() { We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support compile time dependency resolution such as Scala where this is done via implicits. -## Type classes 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 type classes through the extensions mechanisms. - -A syntax that would allow for higher kinds in these definitions may look like this: - -```kotlin -extension interface FunctionK, G<_>> { - fun invoke(fa: F): G -} - -extension object : FunctionK { - fun invoke(fa: Option): List = - fa.fold({ emptyList() }, { listOf(it) }) -} -``` - -Here `F<_>` refers to a type constructor, meaning a type that has a hole within it such as `Option`, `List`, etc. - -A use of this declaration in a polymorphic function would look like: - -```kotlin -fun , A, B> transform(fa: F, f: (A) -> B, with Functor): F = fa.map(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 `Functor` instance defined in scope. -``` - ## Language changes * Add `with` to require evidence of extensions in both function and class/object declarations. From eff56ad2da952257200da3a1a7f1474e4392b012 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:25:32 +0200 Subject: [PATCH 38/73] Remove reified and inline generics section. --- .../compile-time-dependency-resolution.md | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 60dec1639..a604e74b3 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -97,44 +97,6 @@ fetch("1182938") // compiles since we got evidence of a `Repository` fetch("1239821") // does not compile: No `Repository` evidence defined in scope! ``` -## Overcoming `inline` + `reified` limitations - -Extension interface contracts allow us to work around `inline` `reified` generics and their limitations and express those as part of the constraints instead: - -```kotlin -extension interface Reified { - val A.selfClass: KClass -} -``` - -Now a function that was doing something like: - -```kotlin -inline fun foo() { .... A::class ... } -``` - -can be replaced with: - -```kotlin -fun fooTC(with Reified): Klass { .... A.selfClass ... } -``` - -This allows us to obtain generics info without the need to declare functions as `inline` or `reified`, overcoming the current limitation of inline reified functions in that they can't be invoked unless made concrete from non-reified contexts. - -Note that this does not remove the need to use `inline reified` where one tries to inspect generic type information at runtime through reflection. This particular case is only relevant for those cases where you know the types you want `reified` ahead of time and you need to access their class value. - -```kotlin -extension class Foo { - val someKlazz = foo() // won't compile because class disallow reified type args -} -``` - -```kotlin -extension class Foo { - val someKlazz = fooTC() // works and does not require to be inside an `inline reified` context -} -``` - ## Composition and chain of evidences Interface declarations and extension evidences can encode further constraints on their type parameters so that they can be composed nicely: From 33f55fccf38a784e3fcb083dc6a9153ad83c4488 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:58:04 +0200 Subject: [PATCH 39/73] Improve resolution order and add error reporting. --- .../compile-time-dependency-resolution.md | 158 ++++++++++++------ 1 file changed, 107 insertions(+), 51 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index a604e74b3..c4cc6bddb 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -195,7 +195,7 @@ Classical interfaces only permit their implementation at the site of a type defi For those reasons constraint interfaces must be declared in one of the following places (in strict resolution order): -1. Scope of the caller function. +1. Arguments of the caller function. 2. Companion object for the target type (User). 3. Companion object for the constraint interface we're looking for (Repository). 4. Subpackages of the package where the target type (User) to resolve is defined. @@ -205,32 +205,125 @@ All other instances are considered orphan instances and are not allowed. See [Ap Additionally, a constraint extension must not conflict with any other pre-existing extension for the same constraint interface; for the purposes of checking this we use the normal resolution rules. That's what we refer as compiler "coherence". -### Interface-side implementations +#### 1. Arguments of the caller function -This definition site is simple to implement and requires no rules except that the instances occur in the same package. For example, the following implementation is allowed: +It looks into the caller function argument list for an evidence of the required extension. Here, `bothValid()` gets a `Validator` passed in so whenever it needs to resolve it for the inner calls to `validate()`, it'll be able to retrieve it from its own argument list. +```kotlin +fun validate(a: A, with validator: Validator): Boolean = a.isValid() + +fun bothValid(x: A, y: A, with validator: Validator): Boolean = validate(x) && validate(y) +``` +#### 2. Companion object for the target type + +In case there's no evidence at the caller function level, we'll look into the companion of the target type. Let's say we have `Repository` and `User` for the `A` type: ```kotlin -package foo.collections +data class User(val id: Int, val name: String) { + companion object { + extension class UserValidator(): Validator { + override fun User.isValid(): Boolean { + return id > 0 && name.length > 0 + } + } + } +} +``` +That'll be enough for resolving the extension. -interface Monoid { - ... - companion object { - extension object IntMonoid : Monoid { ... } - } +#### 3. Companion object for the constraint interface we're looking for + +In case there's neither evidence in the companion of the target type, we'll look in the companion of the constraint interface: + +```kotlin +interface Validator { + fun A.isValid(): Boolean + + companion object { + extension class GroupValidator(with val userValidator: Validator) : Validator { + override fun Group.isValid(): Boolean { + for (x in users) { + if (!x.isValid()) return false + } + return true + } + } + } } ``` +#### 4. Subpackages of the package where the target type is defined + +The next step would be to look into the subpackages of the package where the target type (`User`) is declared. + ```kotlin -package foo.collections.instances +package com.domain.repository -extension object : Monoid { - ... +import com.domain.User + +extension object UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } + + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } } ``` -### Type-side implementations +Here we got a `Repository` defined in a subpackage of `com.domain`, where the `User` type is defined. -This definition site poses additional complications when you consider multi-parameter type classes. +#### 5. Subpackages of the package where the constraint interface is defined + +Last place to look at would be subpackages of the package where the constraint interface is defined. + +```kotlin +package com.data.instances + +import com.data.Repository +import com.domain.User + +extension object UserRepository: Repository { + override fun loadAll(): List { + return listOf(User(25, "Bob")) + } + + override fun loadById(id: Int): User? { + return if (id == 25) { + User(25, "Bob") + } else { + null + } + } +} +``` + +Here, we are resolving it from `com.data.instances`, which a subpackage of `com.data`, where our constraint `Repository` is defined. + +## Error reporting + +We've got a `CallChecker` in place to report inlined errors using the context trace. That allows us to report as many errors as possible in a single compiler pass. Also provide them in two different formats: + +#### Inline errors while coding (using inspections and red underline) + +Whenever you're coding the checker is running and proper unresolvable extension errors can be reported within IDEA inspections. + +![Screenshot 2019-04-05 at 12 51 11](https://user-images.githubusercontent.com/6547526/55622809-85580480-57a1-11e9-8254-0830d29593ec.png) + +#### Errors once you hit the "compile" button: + +Once you hit the "compile" button or run any compile command you'll also get those errors reported. + +![Screenshot 2019-04-05 at 12 59 54](https://user-images.githubusercontent.com/6547526/55623259-be44a900-57a2-11e9-927e-fe0150265ba5.png) + + +### What's still to be done? + +We have additional complications when you consider multi-parameter constraint interfaces. ```kotlin package foo.collections @@ -270,43 +363,6 @@ To determine whether a type class definition is a valid type-side implementation If a type class implementation meets these rules it is a valid type-side implementation. -## Compile resolution rules - -When the compiler finds a call site of a function that has type class instance constraints declared with `with`, as in the example below: - -Declaration: - -```kotlin -fun add(a: A, b: A, with Monoid): A = a.combine(b) -``` - -Call site: - -```kotlin -fun addInts(): Int = add(1, 2) -``` - -1. The compiler first looks at the function context where the invocation is happening. If a function argument matches the required instance for a type class, it uses that instance; e.g.: - - ```kotlin - fun duplicate(a : A, with M: Monoid): A = a.combine(a) - ``` - - The invocation `a.combine(a)` requires a `Monoid` and since one is passed as an argument to `duplicate`, it uses that one. - -2. In case it fails, it inspects the following places, sequentially, until it is able to find a valid unique instance for the type class: - - * The current package (where the invocation is taking place), as long as the `extension` is `internal`. - * The companion object of the type parameter(s) in the type class (e.g. in `Monoid`, it looks into `A`'s companion object). - * The companion object of the type class. - * The subpackages of the package where the type parameter(s) in the type class is defined. - * The subpackages of the package where the type class is defined. - -3. If no matching implementation is found in either of these places then the code fails to compile. -4. If more than one matching implementation is found, then the code fails to compile and the compiler indicates that there are conflicting instances. - -Some of these examples were proposed by Roman Elizarov and the Arrow contributors where these features were originally discussed: https://github.com/Kotlin/KEEP/pull/87 - ## Appendix A: Orphan implementations Orphan implementations are a subject of controversy. Combining two libraries - one defining a data type, the other defining an interface - is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. From 988c8a9eb35fcb838a625122fa53e65eee49a628 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 11:59:48 +0200 Subject: [PATCH 40/73] Adds how to try sections. --- .../compile-time-dependency-resolution.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index c4cc6bddb..1e2f1a685 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -320,6 +320,28 @@ Once you hit the "compile" button or run any compile command you'll also get tho ![Screenshot 2019-04-05 at 12 59 54](https://user-images.githubusercontent.com/6547526/55623259-be44a900-57a2-11e9-927e-fe0150265ba5.png) +## How to try KEEEP-87? - approach 1 + +- Clone this project and checkout the **keep-87** branch. +- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. +- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. +- Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. +- It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) +) with some sample code that you can try out. + +## How to try KEEEP-87? - Alternative approach (easier) + +- Download the latest version of IntelliJ IDEA 2018.2.4 from JetBrains +- Go to `preferences` -> `plugins` section. +- Click on "Manage Plugin Repositories". +![InstallKeepFromRepository1](https://user-images.githubusercontent.com/6547526/55884351-38609d80-5ba8-11e9-8855-3c570ee8a1af.png) +- Add our Amazon s3 plugin repository as in the image. +![InstallKeepFromRepository2](https://user-images.githubusercontent.com/6547526/55884427-562e0280-5ba8-11e9-98e8-8811e8e3e8b0.png) +- Now browse for "keep87" plugin. +![InstallKeepFromRepository3](https://user-images.githubusercontent.com/6547526/55884468-67770f00-5ba8-11e9-92d6-e9cc8cc3f572.png) +- Install it. +![InstallKeepFromRepository4](https://user-images.githubusercontent.com/6547526/55884479-6b0a9600-5ba8-11e9-8a19-0eec53187fc5.png) +- Download and run [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) on that IntellIJ instance. ### What's still to be done? From 94a1a99d0266adf7e1a6aab9ca38c18ec25b009d Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:02:00 +0200 Subject: [PATCH 41/73] Polish how to test sections. --- proposals/compile-time-dependency-resolution.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 1e2f1a685..85a4ca1bc 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -320,16 +320,18 @@ Once you hit the "compile" button or run any compile command you'll also get tho ![Screenshot 2019-04-05 at 12 59 54](https://user-images.githubusercontent.com/6547526/55623259-be44a900-57a2-11e9-927e-fe0150265ba5.png) -## How to try KEEEP-87? - approach 1 +## How to try KEEEP-87? (First approach) -- Clone this project and checkout the **keep-87** branch. +- Clone [Our Kotlin fork](https://github.com/arrow-kt/kotlin) and checkout the **keep-87** branch. - Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. - Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. - Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. - It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) ) with some sample code that you can try out. -## How to try KEEEP-87? - Alternative approach (easier) +## How to try KEEEP-87? (Alternative approach - easier) + +We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. To use it: - Download the latest version of IntelliJ IDEA 2018.2.4 from JetBrains - Go to `preferences` -> `plugins` section. From bcbd74645bfb54798934ab3ef54f0a302afbaddd Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:09:13 +0200 Subject: [PATCH 42/73] Ploishes Orphan instances description a bit. --- .../compile-time-dependency-resolution.md | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 85a4ca1bc..cdea5109f 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -345,53 +345,57 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ![InstallKeepFromRepository4](https://user-images.githubusercontent.com/6547526/55884479-6b0a9600-5ba8-11e9-8a19-0eec53187fc5.png) - Download and run [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) on that IntellIJ instance. -### What's still to be done? + +## What's still to be done? + +### Type-side implementations We have additional complications when you consider multi-parameter constraint interfaces. ```kotlin -package foo.collections +package foo.repo -interface Isomorphism { +// I stands for the index type, A for the stored type. +interface Repository { ... } ``` ```kotlin -package data.collections.foo +package data.foo -data class Foo(...) -extension class : Isomorphism { +data class Id(...) +extension class RepoIndexedById : Repository { ... } ``` ```kotlin -package data.collections.bar +package data.foo.user -data class Bar(...) -extension class : Isomorphism { +data class User(...) +extension class UserRepository : Repository { ... } ``` The above instances are each defined alongside their respective type definitions and yet they clearly conflict with each other. We will also run into quandaries once we consider generic types. We can crib some prior art from Rust1 to help us out here. -To determine whether a type class definition is a valid type-side implementation we perform the following check: +To determine whether an extension definition is a valid type-side implementation we'd need to perform the following check: -1. A "local type" is any type (but not type alias) defined in the current file (e.g. everything defined in `data.collections.bar` if we're evaluating `data.collections.bar`). +1. A "local type" is any type (but not type alias) defined in the current file (e.g. everything defined in `data.foo.user` if we're evaluating `data.foo.user`). 2. A generic type parameter is "covered" by a type if it occurs within that type (e.g. `MyType` covers `T` in `MyType` but not `Pair`). -3. Write out the parameters to the type class in order. +3. Write out the parameters to the constraint interface in order. 4. The parameters must include a type defined in this file. 5. Any generic type parameters must occur after the first instance of a local type or be covered by a local type. -If a type class implementation meets these rules it is a valid type-side implementation. +**If an extension meets these rules it is a valid type-side implementation.** ## Appendix A: Orphan implementations -Orphan implementations are a subject of controversy. Combining two libraries - one defining a data type, the other defining an interface - is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. +Orphan implementations are a subject of controversy. Combining two libraries - one defining a target type (`User`), the other defining an interface (`Repository`) - is a feature that many programmers have longed for. However, implementing this feature in a way that doesn't break other features of interfaces is difficult and drastically complicates how the compiler works with those interfaces. -Orphan implementations are the reason that type classes have often been described as "anti-modular", as the most common way of dealing with them is through global coherence checks. This is necessary to ensure that two libraries have not defined incompatible implementations of a type class interface. +Orphan implementations are the reason that other implementations of this approach have often been described as "anti-modular", as the most common way of dealing with them is through global coherence checks. This is necessary to ensure that two libraries have not defined incompatible extensions of a given constraint interface. Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans then it's useful to consider how they could be added in the future. From dfc0b645b67c1ef167a54d48e4873ff772a5e93b Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:11:29 +0200 Subject: [PATCH 43/73] Completes first pass. --- .../compile-time-dependency-resolution.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index cdea5109f..86e9ed88e 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -403,13 +403,13 @@ Ideally we want to ban orphan implementations in libraries but not in executable ### Package-based approach to orphans -A simple way to allow orphan implementations is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages, it is possible to do the following. +A simple way to allow orphan extenions is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages, it is possible to do the following. ```kotlin // In some library foo package foo.collections -extension class Monoid { +extension class Repository { ... } ``` @@ -418,25 +418,25 @@ extension class Monoid { // In some application that uses the foo library package foo.collections -extension object : Monoid { +extension object : Repository { ... } ``` -This approach would not forbid orphan implementations in libraries but it would highly discourage libraries from providing them, as this would involve writing code in the package namespace of another library. +This approach would not forbid orphan extensions in libraries but it would highly discourage libraries from providing them, as this would involve writing code in the package namespace of another library. ### Internal modifier-based approach to orphans -An alternate approach is to require that orphan implementations be marked `internal`. The full rules would be as follows: +An alternate approach is to require that orphan extensions be marked `internal`. The full rules would be as follows: -1. All orphan implementations must be marked `internal`. -2. All code which closes over an internal implementation must be marked internal. Code closes over a type class instance if it contains a static reference to such an implementation. -3. Internal implementations defined in the same module are in scope for the current module. -4. Internal implementations defined in other modules are not valid for type class resolution. +1. All orphan extensions must be marked `internal`. +2. All code which closes over an internal extension must be marked internal. Code closes over a constraint interface extension if it contains a static reference to such an extension. +3. Internal extensions defined in the same module are in scope for the current module. +4. Internal extensions defined in other modules are not valid for constraint interface resolution. This approach works well but it has a few problems. -1. It forces applications that use orphan implementations to mark all their code as internal, which is a lot of syntactic noise. +1. It forces applications that use orphan extensions to mark all their code as internal, which is a lot of syntactic noise. 2. It complicates the compiler's resolution mechanism since it's not as easy to enumerate definition sites. The first problem actually leads us to a better solution. From a4fd7f4dfb505465a3168e5670ca4297a0019025 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:25:15 +0200 Subject: [PATCH 44/73] Adds more details to some sections. --- .../compile-time-dependency-resolution.md | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 86e9ed88e..19fdc0db5 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -80,7 +80,7 @@ extension class UserRepository: Repository { ``` **Extensions are named** for now, mostly for supporting Java, but we'd be open to iterate that towards allowing definition through properties and anonymous classes. -We got the contract definition (interface) and the way to provide evidence of an extension, we'd just need to connect both things now. Interfaces can be used to define constraints of a function or a class. We the `with` keyword for that. +We got the contract definition (interface) and the way to provide evidence of an extension, we'd just need to connect both things now. Interfaces can be used to define constraints of a function or a class constructor. We the `with` keyword for that. ```kotlin fun fetchById(id: String, with repository: Repository): A? { @@ -88,13 +88,52 @@ fun fetchById(id: String, with repository: Repository): A? { } ``` -As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. +As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. That means the following two functions are equivalent: + +```kotlin +// Kotlin + KEEP-87 +fun fetchById(id: String, with repository: Repository): A? { + return loadById(id) +} + +// Regular Kotlin +fun fetchById(id: String, repository: Repository): A? = + with (semigroup) { + return loadById(id) + } +``` On the call site: ```kotlin -fetch("1182938") // compiles since we got evidence of a `Repository` in scope. -fetch("1239821") // does not compile: No `Repository` evidence defined in scope! +fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. +fetchById("1239821") // does not compile: No `Repository` evidence defined in scope! +``` + +Functions with extension parameters can be passed all values, or extension ones can be omitted and let the compiler resolve the suitable extensions for them. + +```kotlin +fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. +fetchById("1182938", UserRepository()) // you can provide it manually. +``` + +When used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the with keyword adds the value to the scope of every method in the class. The following classes are equivalent: + +```kotlin +data class Group(val values: List) + +// Kotlin + KEEP-87 +extension class GroupRepository(with val R: Repository) : Repository> { + override fun loadAll(): Group = + Group(R.loadAll()) +} + +// Regular Kotlin +class GroupRepository(val R: Repository) : Repository> { + override fun loadAll(): Group = with (R) { + Group(loadAll()) + } +} ``` ## Composition and chain of evidences From a32eaa515731ae9cadd2f366c7a2767fdb7669f2 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:32:44 +0200 Subject: [PATCH 45/73] Adds some details to description. --- .../compile-time-dependency-resolution.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 19fdc0db5..0141de86b 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -8,13 +8,12 @@ ## Summary -The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we'd want to enable extension contract interfaces to be defined as function or class arguments and enable compiler to automatically resolve and inject those instances that must be provided evidence for in one of a given set of scopes. In case of not having evidence of any of those required interfaces (program constraints), compiler would fail and provide proper error messages. +The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we'd want to enable extension contract interfaces to be defined as function or class constructor arguments and enable compiler to automatically resolve and inject those instances that must be provided evidence for in one of a given set of scopes. In case of not having evidence of any of those required interfaces (program constraints), compiler would fail and provide proper error messages. ## Motivation -* Support extension evidence compile-time verification. +* Support compile-time verification of program dependencies (extensions). * Enable nested extension resolution. -* Enable multiple extension function groups for type declarations. * Support compile-time verification of a program correctness given behavioral constraints are raised to the interface types. * Enable definition of polymorphic functions whose constraints can be verified at compile time in call sites. @@ -33,7 +32,7 @@ interface Repository { The above declaration can serve as a target for implementations for any arbitrary type passed for `A`. -In the implementation below we provide evidence that there is a `Repository` extensions available in scope, enabling both methods defined for the given behavior to work over the `User` type. As you can see, we're enabling a new keyword here: `extension`. +In the implementation below we provide evidence that there is a `Repository` extension available in scope, enabling both methods defined for the given behavior to work over the `User` type. As you can see, we're enabling a new keyword here: `extension`. ```kotlin package com.data.instances @@ -79,8 +78,9 @@ extension class UserRepository: Repository { } ``` -**Extensions are named** for now, mostly for supporting Java, but we'd be open to iterate that towards allowing definition through properties and anonymous classes. -We got the contract definition (interface) and the way to provide evidence of an extension, we'd just need to connect both things now. Interfaces can be used to define constraints of a function or a class constructor. We the `with` keyword for that. +**Extensions are named** for now, mostly with the purpose of supporting Java. We'd be fine with this narrower approach, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. + +Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We the `with` keyword for that. ```kotlin fun fetchById(id: String, with repository: Repository): A? { @@ -88,7 +88,7 @@ fun fetchById(id: String, with repository: Repository): A? { } ``` -As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. That means the following two functions are equivalent: +As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. That means the following two functions would be equivalent: ```kotlin // Kotlin + KEEP-87 @@ -103,21 +103,21 @@ fun fetchById(id: String, repository: Repository): A? = } ``` -On the call site: +On the call site, we could use it like: ```kotlin fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. fetchById("1239821") // does not compile: No `Repository` evidence defined in scope! ``` -Functions with extension parameters can be passed all values, or extension ones can be omitted and let the compiler resolve the suitable extensions for them. +Functions with extension parameters can be passed all values, or extension ones can be omitted and let the compiler resolve the suitable extensions for them. That makes the approach really similar to how default arguments work in Kotlin. ```kotlin fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. fetchById("1182938", UserRepository()) // you can provide it manually. ``` -When used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the with keyword adds the value to the scope of every method in the class. The following classes are equivalent: +When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the with keyword adds the value to the scope of every method in the class. In this scenario, the following classes would be equivalent: ```kotlin data class Group(val values: List) From c811a73b0dd5f09b73e4548665d06d8f8063f603 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:33:34 +0200 Subject: [PATCH 46/73] Moar polishments. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 0141de86b..23028182a 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -138,7 +138,7 @@ class GroupRepository(val R: Repository) : Repository> { ## Composition and chain of evidences -Interface declarations and extension evidences can encode further constraints on their type parameters so that they can be composed nicely: +Constraint interface declarations and extension evidences can encode further constraints on their type parameters so that they can be composed nicely: ```kotlin package com.data.instances From e6ba5e6fd81efbf0f5ef3cd560a516c96bb308d7 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:48:21 +0200 Subject: [PATCH 47/73] Fixes a typo. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 23028182a..06be40292 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -80,7 +80,7 @@ extension class UserRepository: Repository { **Extensions are named** for now, mostly with the purpose of supporting Java. We'd be fine with this narrower approach, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. -Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We the `with` keyword for that. +Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. ```kotlin fun fetchById(id: String, with repository: Repository): A? { From 5485db131f398cda55280b1704359d0c887a2bc3 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:50:29 +0200 Subject: [PATCH 48/73] Fixes a typo. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 06be40292..b7a5c1bde 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -117,7 +117,7 @@ fetchById("1182938") // compiles since we got evidence of a `Repository("1182938", UserRepository()) // you can provide it manually. ``` -When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the with keyword adds the value to the scope of every method in the class. In this scenario, the following classes would be equivalent: +When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. In this scenario, the following classes would be equivalent: ```kotlin data class Group(val values: List) From ed8561e4018bcca87abf888280e3ff0b17553745 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Thu, 11 Apr 2019 12:52:17 +0200 Subject: [PATCH 49/73] Fixes a typo on a snippet. --- proposals/compile-time-dependency-resolution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index b7a5c1bde..6d8a04a1a 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -149,11 +149,11 @@ import com.domain.Group extension class GroupRepository(with val repoA: Repository) : Repository> { override fun loadAll(): List> { - return listOf(Group(userRepository.loadAll())) + return listOf(Group(repoA.loadAll())) } override fun loadById(id: Int): Group? { - return Group(userRepository.loadById(id)) + return Group(repoA.loadById(id)) } } ``` From 535c453844fb99dd1b9690f406cd32f4f59b15ad Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 08:20:23 +0200 Subject: [PATCH 50/73] Includes horizontal composition in Summary. --- proposals/compile-time-dependency-resolution.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 6d8a04a1a..ce5125ac5 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -10,6 +10,10 @@ The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we'd want to enable extension contract interfaces to be defined as function or class constructor arguments and enable compiler to automatically resolve and inject those instances that must be provided evidence for in one of a given set of scopes. In case of not having evidence of any of those required interfaces (program constraints), compiler would fail and provide proper error messages. +This would bring **first-class named extensions families** to Kotlin. Extension families allow us to guarantee a given data type (class, interface, etc.) satisfies behaviors (group of functions) that are decoupled from the type's inheritance hierarchy. + +Extension families favor horizontal composition based on compile-time resolution between types and their extensions vs the traditional subtype style composition where users are forced to extend and implement classes and interfaces. + ## Motivation * Support compile-time verification of program dependencies (extensions). From fba1db3746967810880aefba36ecb4522249b753 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 08:45:58 +0200 Subject: [PATCH 51/73] Updates repo to use Map property and adds A.save() extension. --- .../compile-time-dependency-resolution.md | 123 +++++++++++------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index ce5125ac5..efd493899 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -29,8 +29,9 @@ We propose to use the existing `interface` semantics, allowing for generic defin package com.data interface Repository { - fun loadAll(): List + fun loadAll(): Map fun loadById(id: Int): A? + fun A.save(): Unit } ``` @@ -44,17 +45,20 @@ package com.data.instances import com.data.Repository import com.domain.User -extension object UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) +extension object UserRepository : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` @@ -68,16 +72,19 @@ import com.data.Repository import com.domain.User extension class UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` @@ -152,12 +159,16 @@ import com.domain.User import com.domain.Group extension class GroupRepository(with val repoA: Repository) : Repository> { - override fun loadAll(): List> { - return listOf(Group(repoA.loadAll())) + override fun loadAll(): Map> { + return repoA.loadAll().mapValues { Group(it.value) } } override fun loadById(id: Int): Group? { - return Group(repoA.loadById(id)) + return repoA.loadById(id)?.let { Group(it) } + } + + override fun Group.save() { + this.items.map { repoA.run { it.save() } } } } ``` @@ -199,17 +210,20 @@ fun fetch(id: String, with R: Repository): A = loadById(id) // function p #### Extension evidence using an Object ```kotlin -extension object UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) +extension object UserRepository : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` @@ -218,16 +232,19 @@ extension object UserRepository: Repository { ```kotlin extension class UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` @@ -303,17 +320,20 @@ package com.domain.repository import com.domain.User -extension object UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) +extension object UserRepository : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` @@ -330,17 +350,20 @@ package com.data.instances import com.data.Repository import com.domain.User -extension object UserRepository: Repository { - override fun loadAll(): List { - return listOf(User(25, "Bob")) +extension object UserRepository : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers } override fun loadById(id: Int): User? { - return if (id == 25) { - User(25, "Bob") - } else { - null - } + return storedUsers[id] + } + + override fun User.save() { + storedUsers[this.id] = this } } ``` From c563312715adadef27148740fb8d361af85721a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Fri, 12 Apr 2019 08:52:34 +0200 Subject: [PATCH 52/73] Update proposals/compile-time-dependency-resolution.md Co-Authored-By: JorgeCastilloPrz --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index efd493899..6286a0844 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -276,7 +276,7 @@ fun bothValid(x: A, y: A, with validator: Validator): Boolean = validate( #### 2. Companion object for the target type -In case there's no evidence at the caller function level, we'll look into the companion of the target type. Let's say we have `Repository` and `User` for the `A` type: +In case there's no evidence at the caller function level, we'll look into the companion of the target type. Let's say we have an extension of `Validator`: ```kotlin data class User(val id: Int, val name: String) { companion object { From 21aeaed4717c2897cbd851da2ebe3891bae2bff5 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 08:54:21 +0200 Subject: [PATCH 53/73] swap How to try approaches. --- .../compile-time-dependency-resolution.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index efd493899..b9228c167 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -386,16 +386,7 @@ Once you hit the "compile" button or run any compile command you'll also get tho ![Screenshot 2019-04-05 at 12 59 54](https://user-images.githubusercontent.com/6547526/55623259-be44a900-57a2-11e9-927e-fe0150265ba5.png) -## How to try KEEEP-87? (First approach) - -- Clone [Our Kotlin fork](https://github.com/arrow-kt/kotlin) and checkout the **keep-87** branch. -- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. -- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. -- Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. -- It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) -) with some sample code that you can try out. - -## How to try KEEEP-87? (Alternative approach - easier) +## How to try KEEP-87? We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. To use it: @@ -411,6 +402,15 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ![InstallKeepFromRepository4](https://user-images.githubusercontent.com/6547526/55884479-6b0a9600-5ba8-11e9-8a19-0eec53187fc5.png) - Download and run [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) on that IntellIJ instance. +## How to try KEEP-87? (Alternative approach) + +- Clone [Our Kotlin fork](https://github.com/arrow-kt/kotlin) and checkout the **keep-87** branch. +- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. +- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. +- Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. +- It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) +) with some sample code that you can try out. + ## What's still to be done? From c8bf8693d8fb31e46b158b1c93958bf5de6f2aa3 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 08:56:09 +0200 Subject: [PATCH 54/73] Fixes a typo in one of the snippets where a monoid was wrongly mentioned. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index f558d4f60..8989570de 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -109,7 +109,7 @@ fun fetchById(id: String, with repository: Repository): A? { // Regular Kotlin fun fetchById(id: String, repository: Repository): A? = - with (semigroup) { + with (repository) { return loadById(id) } ``` From 7792dd78fdcd02efbae7791b48f8b2213189c11a Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 08:59:32 +0200 Subject: [PATCH 55/73] Adds proper package to User definition snippet. --- proposals/compile-time-dependency-resolution.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 8989570de..94029acfd 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -278,6 +278,8 @@ fun bothValid(x: A, y: A, with validator: Validator): Boolean = validate( In case there's no evidence at the caller function level, we'll look into the companion of the target type. Let's say we have an extension of `Validator`: ```kotlin +package com.domain + data class User(val id: Int, val name: String) { companion object { extension class UserValidator(): Validator { From 67c8f3ef1c2670db6ab27ce66387ff3c26c76b79 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:05:53 +0200 Subject: [PATCH 56/73] Polishes wording over resolution order. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 94029acfd..201c6967f 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -253,7 +253,7 @@ extension class UserRepository: Repository { Classical interfaces only permit their implementation at the site of a type definition. Compiler extension resolution pattern typically relax this rule and **allow extension evidences be declared outside of the type definition**. When relaxing this rule it is important to preserve the coherency we take for granted with classical interfaces. -For those reasons constraint interfaces must be declared in one of the following places (in strict resolution order): +For those reasons constraint interfaces must be provided in one of the following scopes (in strict resolution order): 1. Arguments of the caller function. 2. Companion object for the target type (User). From 6ba257772a274f3af3db82cc8ea05a25c17b1d94 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:10:48 +0200 Subject: [PATCH 57/73] Drops implicits from comparison. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 201c6967f..345ebb022 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -184,7 +184,7 @@ fun main() { } ``` -We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support compile time dependency resolution such as Scala where this is done via implicits. +We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support compile time dependency resolution. ## Language changes From 87d1c4325527fc85d332c3e53991055bee7e8554 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:12:45 +0200 Subject: [PATCH 58/73] Fix wrong typing in loadById call sites. --- proposals/compile-time-dependency-resolution.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 345ebb022..b0db3916a 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -94,7 +94,7 @@ extension class UserRepository: Repository { Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. ```kotlin -fun fetchById(id: String, with repository: Repository): A? { +fun fetchById(id: Int, with repository: Repository): A? { return loadById(id) // Repository syntax is automatically activated inside the function scope! } ``` @@ -103,12 +103,12 @@ As you can see, we get the constraint syntax automatically active inside the fun ```kotlin // Kotlin + KEEP-87 -fun fetchById(id: String, with repository: Repository): A? { +fun fetchById(id: Int, with repository: Repository): A? { return loadById(id) } // Regular Kotlin -fun fetchById(id: String, repository: Repository): A? = +fun fetchById(id: Int, repository: Repository): A? = with (repository) { return loadById(id) } @@ -117,15 +117,15 @@ fun fetchById(id: String, repository: Repository): A? = On the call site, we could use it like: ```kotlin -fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. -fetchById("1239821") // does not compile: No `Repository` evidence defined in scope! +fetchById(11829) // compiles since we got evidence of a `Repository` in scope. +fetchById(12398) // does not compile: No `Repository` evidence defined in scope! ``` Functions with extension parameters can be passed all values, or extension ones can be omitted and let the compiler resolve the suitable extensions for them. That makes the approach really similar to how default arguments work in Kotlin. ```kotlin -fetchById("1182938") // compiles since we got evidence of a `Repository` in scope. -fetchById("1182938", UserRepository()) // you can provide it manually. +fetchById(11829) // compiles since we got evidence of a `Repository` in scope. +fetchById(11829, UserRepository()) // you can provide it manually. ``` When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. In this scenario, the following classes would be equivalent: From 531711d848327eadfc9a9b6fa9dd38e2f8c525be Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:14:32 +0200 Subject: [PATCH 59/73] Rethink wording about named extensions. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index b0db3916a..acf19ee4e 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -89,7 +89,7 @@ extension class UserRepository: Repository { } ``` -**Extensions are named** for now, mostly with the purpose of supporting Java. We'd be fine with this narrower approach, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. +In the KEEP as it's coded now, **Extensions are named**. That's mostly with the purpose of supporting Java. We'd be fine with this narrower approach we're providing, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. From bd1416d1b6376ed2f7aa1569bd853de9fe069ae1 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:14:32 +0200 Subject: [PATCH 60/73] Rethink wording about named extensions. --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index b0db3916a..bfa7ca4f3 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -89,7 +89,7 @@ extension class UserRepository: Repository { } ``` -**Extensions are named** for now, mostly with the purpose of supporting Java. We'd be fine with this narrower approach, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. +In the KEEP as it's coded now, **extensions are named**. That's mostly with the purpose of supporting Java. We'd be fine with this narrower approach we're providing, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. From 87eafe69174ef8b6bee2528b09b352387326b3a0 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:36:19 +0200 Subject: [PATCH 61/73] Remove FP constructs mentions from Error reporting screenshots. --- proposals/compile-time-dependency-resolution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index bfa7ca4f3..7e41d2fd4 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -380,13 +380,13 @@ We've got a `CallChecker` in place to report inlined errors using the context tr Whenever you're coding the checker is running and proper unresolvable extension errors can be reported within IDEA inspections. -![Screenshot 2019-04-05 at 12 51 11](https://user-images.githubusercontent.com/6547526/55622809-85580480-57a1-11e9-8254-0830d29593ec.png) +![Idea Inspections](https://user-images.githubusercontent.com/6547526/56019955-43c9db00-5d06-11e9-8780-5fb12c55f9ff.png) #### Errors once you hit the "compile" button: Once you hit the "compile" button or run any compile command you'll also get those errors reported. -![Screenshot 2019-04-05 at 12 59 54](https://user-images.githubusercontent.com/6547526/55623259-be44a900-57a2-11e9-927e-fe0150265ba5.png) +![Idea Inspections](https://user-images.githubusercontent.com/6547526/56019952-43314480-5d06-11e9-956c-dc161dd38835.png) ## How to try KEEP-87? From 3753301769bd592bea43c4c3d2afbfcd5f8559b4 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 09:48:33 +0200 Subject: [PATCH 62/73] Refactor keep to reflect proper wording on first resolution scope. --- proposals/compile-time-dependency-resolution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 7e41d2fd4..ee76f6955 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -380,13 +380,13 @@ We've got a `CallChecker` in place to report inlined errors using the context tr Whenever you're coding the checker is running and proper unresolvable extension errors can be reported within IDEA inspections. -![Idea Inspections](https://user-images.githubusercontent.com/6547526/56019955-43c9db00-5d06-11e9-8780-5fb12c55f9ff.png) +![Idea Inspections](https://user-images.githubusercontent.com/6547526/56020688-fea6a880-5d07-11e9-906a-9d085565eee2.png) #### Errors once you hit the "compile" button: Once you hit the "compile" button or run any compile command you'll also get those errors reported. -![Idea Inspections](https://user-images.githubusercontent.com/6547526/56019952-43314480-5d06-11e9-956c-dc161dd38835.png) +![Idea Inspections](https://user-images.githubusercontent.com/6547526/56020690-00706c00-5d08-11e9-8cbd-ba4b852b9105.png) ## How to try KEEP-87? From 951ea3d6d2a6a4327e8dad30e33c22aaf72c4880 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Fri, 12 Apr 2019 16:37:42 +0200 Subject: [PATCH 63/73] Reflect how syntax becomes available in methods and functions bodies. --- .../compile-time-dependency-resolution.md | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index ee76f6955..f47e550ca 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -128,25 +128,62 @@ fetchById(11829) // compiles since we got evidence of a `Repository` fetchById(11829, UserRepository()) // you can provide it manually. ``` -When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. In this scenario, the following classes would be equivalent: +When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. To showcase that, let's say we have a `Validator`, like: + +```kotlin +interface Validator { + fun A.isValid(): Boolean +} +``` + +In this scenario, the following classes would be equivalent: ```kotlin data class Group(val values: List) // Kotlin + KEEP-87 -extension class GroupRepository(with val R: Repository) : Repository> { - override fun loadAll(): Group = - Group(R.loadAll()) +extension class ValidatedRepository(with val V: Validator) : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + return storedUsers.filter { it.value.isValid() } + } + + override fun loadById(id: Int): A? { + return storedUsers[id]?.let { if (it.isValid()) it else null } + } + + override fun A.save() { + storedUsers[generateKey(this)] = this + } } // Regular Kotlin -class GroupRepository(val R: Repository) : Repository> { - override fun loadAll(): Group = with (R) { - Group(loadAll()) - } +class ValidatedRepository(val V: Validator) : Repository { + + val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB + + override fun loadAll(): Map { + with (V) { + return storedUsers.filter { it.value.isValid() } + } + } + + override fun loadById(id: Int): A? { + with (V) { + return storedUsers[id]?.let { if (it.isValid()) it else null } + } + } + + override fun A.save() { + storedUsers[generateKey(this)] = this + } } ``` +As you can see on the first example, `A.isValid()` becomes automatically available inside the methods scope. The equivalence for that without the KEEP-87 would be to manually use `with (V)` inside each one of them, as you can see in the second example. + ## Composition and chain of evidences Constraint interface declarations and extension evidences can encode further constraints on their type parameters so that they can be composed nicely: @@ -176,7 +213,7 @@ extension class GroupRepository(with val repoA: Repository) : Repository>` as long as there is a `Repository` in scope. Call site would be like: ```kotlin -fun fetchGroup(with repo: GroupRepository) = repo.loadAll() +fun fetchGroup(with repo: GroupRepository) = loadAll() fun main() { fetchGroup() // Succeeds! There's evidence of Repository> and Repository provided in scope. From 297e65f9e453de0f9d42bbd6664b8352473078dc Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 10:27:41 +0200 Subject: [PATCH 64/73] Adds fun and val extension providers to the TODO list. --- .../compile-time-dependency-resolution.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index f47e550ca..d0413f988 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -453,6 +453,30 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ## What's still to be done? +### Function and property extension providers + +Ideally we'd enable users to provide extensions also using `val` and `fun`. They'd look similar to: + +```kotlin +// Simple fun extension provisioning +extension fun userRepository(): Repository = object : Repository() { + /* ... */ +} + +// Chained fun extension provisioning (would require both to resolve). +extension fun userValidator(): Validator = UserValidator() +extension fun userRepository(with validator: Validator) : Repository = UserRepository(validator) + +// Simple extension provisioning +extension val userRepository: Repository = UserRepository() + +// Chained extension provisioning (would require both to resolve). +extension val userValidator: Validator = UserValidator() +extension val userRepository: Repository = UserRepository(userValidator) +``` + +In chained extension provisioning, all chained extensions would be required from the caller scope, but not necessarily all provided in the same resolution scope. E.g: `Repository` could be provided in a different resolution scope than `Validator`, and the program would still compile successfully as long as both are available. + ### Type-side implementations We have additional complications when you consider multi-parameter constraint interfaces. From 071d6b7954cbf5d62df7eb28593673ecb969faa3 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 10:35:49 +0200 Subject: [PATCH 65/73] Adds both big still open issues to the KEEP. --- proposals/compile-time-dependency-resolution.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index d0413f988..fb0cb6842 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -453,6 +453,14 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ## What's still to be done? +### Instance resolution based on inheritance + +Some scenarios are not covered yet given some knowledge we are lacking about how subtyping resolution rules are coded in Kotlin compiler. The different scenarios would be required for a fully working compile time extension resolution feature, and [are described in detail here](https://github.com/arrow-kt/kotlin/issues/15). + +### Using extensions in inlined lambdas + +Inlined functions get into trouble when it comes to capture resolved extensions.[The problem is described here](https://github.com/arrow-kt/kotlin/issues/14). + ### Function and property extension providers Ideally we'd enable users to provide extensions also using `val` and `fun`. They'd look similar to: From 6ac0d48b8d35e85622b9f8676d56aef4c8544487 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 12:07:04 +0200 Subject: [PATCH 66/73] Uploads and links fixed Keep87Sample zip project using proper internal modifier where needed. --- proposals/compile-time-dependency-resolution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index fb0cb6842..d77f7851d 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -439,7 +439,7 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ![InstallKeepFromRepository3](https://user-images.githubusercontent.com/6547526/55884468-67770f00-5ba8-11e9-92d6-e9cc8cc3f572.png) - Install it. ![InstallKeepFromRepository4](https://user-images.githubusercontent.com/6547526/55884479-6b0a9600-5ba8-11e9-8a19-0eec53187fc5.png) -- Download and run [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) on that IntellIJ instance. +- Download and run [The Keep87Sample project](https://github.com/47deg/KEEP/files/3079552/Keep87Sample.zip) on that IntellIJ instance. ## How to try KEEP-87? (Alternative approach) @@ -447,7 +447,7 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. - Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. - Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. - Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. -- It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [this project](https://github.com/arrow-kt/kotlin/files/3064100/Keep87Sample.zip) +- It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [The Keep87Sample project](https://github.com/47deg/KEEP/files/3079552/Keep87Sample.zip) ) with some sample code that you can try out. From e564b61430d654f81f541ccf2dbf784ca620bba8 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 12:18:54 +0200 Subject: [PATCH 67/73] Adds clarification for internal modifier requirement in some scopes. --- proposals/compile-time-dependency-resolution.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index d77f7851d..14b048429 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -295,8 +295,8 @@ For those reasons constraint interfaces must be provided in one of the following 1. Arguments of the caller function. 2. Companion object for the target type (User). 3. Companion object for the constraint interface we're looking for (Repository). -4. Subpackages of the package where the target type (User) to resolve is defined. -5. Subpackages of the package where the constraint interface (Repository) is defined. +4. Subpackages of the package where the target type (User) to resolve is defined, **under the same gradle module**. The extension needs to be marked as `internal`. +5. Subpackages of the package where the constraint interface (Repository) is defined, **under the same gradle module**. The extension needs to be marked as `internal`. All other instances are considered orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. @@ -352,7 +352,7 @@ interface Validator { #### 4. Subpackages of the package where the target type is defined -The next step would be to look into the subpackages of the package where the target type (`User`) is declared. +The next step would be to look into the subpackages of the package where the target type (`User`) is declared. It'll just look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. ```kotlin package com.domain.repository @@ -381,7 +381,7 @@ Here we got a `Repository` defined in a subpackage of `com.domain`, where #### 5. Subpackages of the package where the constraint interface is defined -Last place to look at would be subpackages of the package where the constraint interface is defined. +Last place to look at would be subpackages of the package where the constraint interface is defined. It'll just look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. ```kotlin package com.data.instances From 21003c6ce3a816a793243b0ba5ae9fb54d5371f2 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 12:23:46 +0200 Subject: [PATCH 68/73] Switches chained to nested wording. --- proposals/compile-time-dependency-resolution.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 14b048429..444156dd7 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -471,19 +471,19 @@ extension fun userRepository(): Repository = object : Repository() { /* ... */ } -// Chained fun extension provisioning (would require both to resolve). +// Nested fun extension provisioning (would require both to resolve). extension fun userValidator(): Validator = UserValidator() extension fun userRepository(with validator: Validator) : Repository = UserRepository(validator) // Simple extension provisioning extension val userRepository: Repository = UserRepository() -// Chained extension provisioning (would require both to resolve). +// Nested extension provisioning (would require both to resolve). extension val userValidator: Validator = UserValidator() extension val userRepository: Repository = UserRepository(userValidator) ``` -In chained extension provisioning, all chained extensions would be required from the caller scope, but not necessarily all provided in the same resolution scope. E.g: `Repository` could be provided in a different resolution scope than `Validator`, and the program would still compile successfully as long as both are available. +In nested extension provisioning, all nested extensions would be required from the caller scope, but not necessarily all provided in the same resolution scope. E.g: `Repository` could be provided in a different resolution scope than `Validator`, and the program would still compile successfully as long as both are available. ### Type-side implementations From 83a1a9490926502c12a12d2c1c5531e937c28a34 Mon Sep 17 00:00:00 2001 From: Jorge Castillo Date: Mon, 15 Apr 2019 12:25:45 +0200 Subject: [PATCH 69/73] Adds internal flag to required snippet extensions. --- proposals/compile-time-dependency-resolution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 444156dd7..32d85d2fb 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -359,7 +359,7 @@ package com.domain.repository import com.domain.User -extension object UserRepository : Repository { +internal extension object UserRepository : Repository { val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB @@ -389,7 +389,7 @@ package com.data.instances import com.data.Repository import com.domain.User -extension object UserRepository : Repository { +internal extension object UserRepository : Repository { val storedUsers: MutableMap = mutableMapOf() // e.g. users stored in a DB From abcd5477d4c9e346c305f30746bc00d2dd5127f1 Mon Sep 17 00:00:00 2001 From: Maureen Date: Tue, 16 Apr 2019 16:52:24 -0700 Subject: [PATCH 70/73] small lang changes --- .../compile-time-dependency-resolution.md | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 32d85d2fb..a7d72d095 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -8,22 +8,22 @@ ## Summary -The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we'd want to enable extension contract interfaces to be defined as function or class constructor arguments and enable compiler to automatically resolve and inject those instances that must be provided evidence for in one of a given set of scopes. In case of not having evidence of any of those required interfaces (program constraints), compiler would fail and provide proper error messages. +The goal of this proposal is to enable **compile time dependency resolution** through extension syntax. Overall, we want to enable extension contract interfaces to be declared as constraints in function or class constructor arguments and enable the compiler to automatically resolve and inject those instances that must be provided with evidence in one of a given set of scopes. We'll cover these later in the Extension resolution order section. In the case of not having evidence of a required interface (program constraints), the compiler would fail and provide the proper error messages. -This would bring **first-class named extensions families** to Kotlin. Extension families allow us to guarantee a given data type (class, interface, etc.) satisfies behaviors (group of functions) that are decoupled from the type's inheritance hierarchy. +This would bring **first-class named extension families** to Kotlin. Extension families allow us to guarantee that a given data type (class, interface, etc.) satisfies behaviors (a group of functions) that are decoupled from the type's inheritance hierarchy. -Extension families favor horizontal composition based on compile-time resolution between types and their extensions vs the traditional subtype style composition where users are forced to extend and implement classes and interfaces. +Unlike the traditional subtype style composition where users are forced to extend and implement classes and interfaces, extension families favor horizontal composition based on compile time resolution between types and their extensions. ## Motivation * Support compile-time verification of program dependencies (extensions). * Enable nested extension resolution. -* Support compile-time verification of a program correctness given behavioral constraints are raised to the interface types. -* Enable definition of polymorphic functions whose constraints can be verified at compile time in call sites. +* Support compile-time verification of the correctness of a program give that behavioral constraints are raised to the interface types. +* Enable the definition of polymorphic functions whose constraints can be verified at compile time in call sites. ## Description -We propose to use the existing `interface` semantics, allowing for generic definition of behaviors and their instances in the same style interfaces are defined. +We propose to use the existing `interface` semantics, allowing for a generic definition of behaviors and their instances in the same style that's used to define interfaces. ```kotlin package com.data @@ -37,7 +37,7 @@ interface Repository { The above declaration can serve as a target for implementations for any arbitrary type passed for `A`. -In the implementation below we provide evidence that there is a `Repository` extension available in scope, enabling both methods defined for the given behavior to work over the `User` type. As you can see, we're enabling a new keyword here: `extension`. +In the implementation below, we provide evidence that there is a `Repository` extension available in scope, enabling both methods defined for the given behavior to work over the `User` type. As you can see, we're enabling a new keyword here: `extension`. ```kotlin package com.data.instances @@ -89,9 +89,10 @@ extension class UserRepository: Repository { } ``` -In the KEEP as it's coded now, **extensions are named**. That's mostly with the purpose of supporting Java. We'd be fine with this narrower approach we're providing, but we'd be open to iterate that towards allowing definition through properties and anonymous classes, if there's a need for it. +In KEEP, as it’s coded now, **extensions are named** primarily with the purpose of supporting Java. We’d be fine with this narrower approach that we’re illustrating here, but would be open to iterating towards allowing definition through properties and anonymous classes if there’s a need for it. -Now we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we'd just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. + +Now that we've got the constraint definition (interface) and a way to provide evidence of an extension for it, we just need to connect the dots. Interfaces can be used to define constraints of a function or a class constructor. We use the `with` keyword for that. ```kotlin fun fetchById(id: Int, with repository: Repository): A? { @@ -99,7 +100,8 @@ fun fetchById(id: Int, with repository: Repository): A? { } ``` -As you can see, we get the constraint syntax automatically active inside the function scope, so we can call it's functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. That means the following two functions would be equivalent: +As you can see, the constraint syntax is automatically active inside the function scope, so we can call its functions at will. That's because we consider `Repository` a constraint of our program at this point. In other words, the program cannot work without it, it's a requirement. That means the following two functions would be equivalent: + ```kotlin // Kotlin + KEEP-87 @@ -114,21 +116,21 @@ fun fetchById(id: Int, repository: Repository): A? = } ``` -On the call site, we could use it like: +On the call site, we could use it as follows: ```kotlin fetchById(11829) // compiles since we got evidence of a `Repository` in scope. fetchById(12398) // does not compile: No `Repository` evidence defined in scope! ``` -Functions with extension parameters can be passed all values, or extension ones can be omitted and let the compiler resolve the suitable extensions for them. That makes the approach really similar to how default arguments work in Kotlin. +All values can be passed to functions with extension parameters, or we can omit extension parameters and let the compiler resolve the suitable extensions for them. This makes the approach really similar to the way default arguments work in Kotlin. ```kotlin fetchById(11829) // compiles since we got evidence of a `Repository` in scope. fetchById(11829, UserRepository()) // you can provide it manually. ``` -When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. To showcase that, let's say we have a `Validator`, like: +When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. To showcase this, let's say we have a `Validator`, like: ```kotlin interface Validator { @@ -182,7 +184,7 @@ class ValidatedRepository(val V: Validator) : Repository { } ``` -As you can see on the first example, `A.isValid()` becomes automatically available inside the methods scope. The equivalence for that without the KEEP-87 would be to manually use `with (V)` inside each one of them, as you can see in the second example. +As you can see in the first example, `A.isValid()` becomes available inside the method’s scope automatically. The equivalent version of doing this without KEEP-87 is to manually add `with (V)` inside each method, as you can see in the second example. ## Composition and chain of evidences @@ -210,7 +212,7 @@ extension class GroupRepository(with val repoA: Repository) : Repository>` as long as there is a `Repository` in scope. Call site would be like: +The above extension provides evidence of a `Repository>` as long as there is a `Repository` in scope. The Call site would look like: ```kotlin fun fetchGroup(with repo: GroupRepository) = loadAll() @@ -221,7 +223,8 @@ fun main() { } ``` -We believe the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other langs that also support compile time dependency resolution. +We believe that the encoding proposed above fits nicely with Kotlin's philosophy of extensions and will reduce the boilerplate compared to other languages that also support compile-time dependency resolution. + ## Language changes @@ -288,9 +291,9 @@ extension class UserRepository: Repository { ## Extension resolution order -Classical interfaces only permit their implementation at the site of a type definition. Compiler extension resolution pattern typically relax this rule and **allow extension evidences be declared outside of the type definition**. When relaxing this rule it is important to preserve the coherency we take for granted with classical interfaces. +Classical interfaces only permit their implementation at the site of a type definition. Compiler extension resolution patterns typically relax this rule and **allow extension evidences to be declared outside of the type definition**. When relaxing this rule, it is important to preserve the coherency we take for granted with classical interfaces. +For those reasons, constraint interfaces must be provided in one of the following scopes (in strict resolution order): -For those reasons constraint interfaces must be provided in one of the following scopes (in strict resolution order): 1. Arguments of the caller function. 2. Companion object for the target type (User). @@ -300,11 +303,11 @@ For those reasons constraint interfaces must be provided in one of the following All other instances are considered orphan instances and are not allowed. See [Appendix A](#Appendix-A) for a modification to this proposal that allows for orphan instances. -Additionally, a constraint extension must not conflict with any other pre-existing extension for the same constraint interface; for the purposes of checking this we use the normal resolution rules. That's what we refer as compiler "coherence". +Additionally, a constraint extension must not conflict with any other pre-existing extension for the same constraint interface; for the purpose of checking this, we use the normal resolution rules. That's what we refer to as compiler "coherence". #### 1. Arguments of the caller function -It looks into the caller function argument list for an evidence of the required extension. Here, `bothValid()` gets a `Validator` passed in so whenever it needs to resolve it for the inner calls to `validate()`, it'll be able to retrieve it from its own argument list. +It looks into the caller function argument list for an evidence of the required extension. Here, `bothValid()` gets a `Validator` passed in so, whenever it needs to resolve it for the inner calls to `validate()`, it'll be able to retrieve it from its own argument list. ```kotlin fun validate(a: A, with validator: Validator): Boolean = a.isValid() @@ -331,7 +334,7 @@ That'll be enough for resolving the extension. #### 3. Companion object for the constraint interface we're looking for -In case there's neither evidence in the companion of the target type, we'll look in the companion of the constraint interface: +In case neither evidence exists in the companion of the target type, we'll look in the companion of the constraint interface: ```kotlin interface Validator { @@ -352,7 +355,7 @@ interface Validator { #### 4. Subpackages of the package where the target type is defined -The next step would be to look into the subpackages of the package where the target type (`User`) is declared. It'll just look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. +The next step would be to look into the subpackages of the package where the target type (`User`) is declared. It will simply look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. ```kotlin package com.domain.repository @@ -381,7 +384,7 @@ Here we got a `Repository` defined in a subpackage of `com.domain`, where #### 5. Subpackages of the package where the constraint interface is defined -Last place to look at would be subpackages of the package where the constraint interface is defined. It'll just look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. +The last place to look would be the subpackages of the package where the constraint interface is defined. t will simply look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. ```kotlin package com.data.instances @@ -407,7 +410,7 @@ internal extension object UserRepository : Repository { } ``` -Here, we are resolving it from `com.data.instances`, which a subpackage of `com.data`, where our constraint `Repository` is defined. +Here, the constraint is resolved by finding a valid evidence for it in the `com.data.instances`, which a subpackage of `com.data`, where our constraint `Repository` is defined. ## Error reporting @@ -415,27 +418,27 @@ We've got a `CallChecker` in place to report inlined errors using the context tr #### Inline errors while coding (using inspections and red underline) -Whenever you're coding the checker is running and proper unresolvable extension errors can be reported within IDEA inspections. +The checker is running whenever you’re coding and proper unresolvable extension errors can be reported within IDEA inspections. ![Idea Inspections](https://user-images.githubusercontent.com/6547526/56020688-fea6a880-5d07-11e9-906a-9d085565eee2.png) #### Errors once you hit the "compile" button: -Once you hit the "compile" button or run any compile command you'll also get those errors reported. +You will also get a report of those errors once you hit the "compile" button or run any compile command. ![Idea Inspections](https://user-images.githubusercontent.com/6547526/56020690-00706c00-5d08-11e9-8cbd-ba4b852b9105.png) ## How to try KEEP-87? -We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. To use it: +KEEP-87 is currently deployed to our own Idea plugin repository over Amazon s3. To use it: - Download the latest version of IntelliJ IDEA 2018.2.4 from JetBrains - Go to `preferences` -> `plugins` section. - Click on "Manage Plugin Repositories". ![InstallKeepFromRepository1](https://user-images.githubusercontent.com/6547526/55884351-38609d80-5ba8-11e9-8855-3c570ee8a1af.png) -- Add our Amazon s3 plugin repository as in the image. +- Add our Amazon s3 plugin repository as seen in the image below. ![InstallKeepFromRepository2](https://user-images.githubusercontent.com/6547526/55884427-562e0280-5ba8-11e9-98e8-8811e8e3e8b0.png) -- Now browse for "keep87" plugin. +- Now browse for the "keep87" plugin. ![InstallKeepFromRepository3](https://user-images.githubusercontent.com/6547526/55884468-67770f00-5ba8-11e9-92d6-e9cc8cc3f572.png) - Install it. ![InstallKeepFromRepository4](https://user-images.githubusercontent.com/6547526/55884479-6b0a9600-5ba8-11e9-8a19-0eec53187fc5.png) @@ -444,26 +447,26 @@ We've got the Keep 87 deployed to our own Idea plugin repository over Amazon s3. ## How to try KEEP-87? (Alternative approach) - Clone [Our Kotlin fork](https://github.com/arrow-kt/kotlin) and checkout the **keep-87** branch. -- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) configure the necessary JVMs. +- Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#build-environment-requirements) to configure the necessary JVMs. - Follow the instructions on the [README](https://github.com/arrow-kt/kotlin/blob/master/ReadMe.md#-working-with-the-project-in-intellij-idea) to open the project in IntelliJ IDEA. - Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing `./gradlew runIde`. There is also a pre-configured run configuration titled **IDEA** that does this. - It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download [The Keep87Sample project](https://github.com/47deg/KEEP/files/3079552/Keep87Sample.zip) -) with some sample code that you can try out. +) that includes some sample code that you can try out. ## What's still to be done? ### Instance resolution based on inheritance -Some scenarios are not covered yet given some knowledge we are lacking about how subtyping resolution rules are coded in Kotlin compiler. The different scenarios would be required for a fully working compile time extension resolution feature, and [are described in detail here](https://github.com/arrow-kt/kotlin/issues/15). +Some scenarios aren’t covered yet, given we’re lacking some knowledge about how subtyping resolution rules are coded in the Kotlin compiler. These scenarios would be required for a fully working compile-time extension resolution feature and [are described in detail here](https://github.com/arrow-kt/kotlin/issues/15). ### Using extensions in inlined lambdas -Inlined functions get into trouble when it comes to capture resolved extensions.[The problem is described here](https://github.com/arrow-kt/kotlin/issues/14). +Inlined functions get into trouble when it comes to capturing resolved extensions. [The problem is described here](https://github.com/arrow-kt/kotlin/issues/14). ### Function and property extension providers -Ideally we'd enable users to provide extensions also using `val` and `fun`. They'd look similar to: +Ideally, we'd enable users to provide extensions also using `val` and `fun`. They'd look similar to: ```kotlin // Simple fun extension provisioning @@ -516,9 +519,9 @@ extension class UserRepository : Repository { } ``` -The above instances are each defined alongside their respective type definitions and yet they clearly conflict with each other. We will also run into quandaries once we consider generic types. We can crib some prior art from Rust1 to help us out here. +The above instances are defined alongside their respective type definitions and yet they clearly conflict with each other. We will also run into quandaries once we consider generic types. We can crib some prior art from Rust1 to help us out here. -To determine whether an extension definition is a valid type-side implementation we'd need to perform the following check: +To determine whether an extension definition is a valid type-side implementation, we'd need to perform the following check: 1. A "local type" is any type (but not type alias) defined in the current file (e.g. everything defined in `data.foo.user` if we're evaluating `data.foo.user`). 2. A generic type parameter is "covered" by a type if it occurs within that type (e.g. `MyType` covers `T` in `MyType` but not `Pair`). @@ -534,13 +537,13 @@ Orphan implementations are a subject of controversy. Combining two libraries - o Orphan implementations are the reason that other implementations of this approach have often been described as "anti-modular", as the most common way of dealing with them is through global coherence checks. This is necessary to ensure that two libraries have not defined incompatible extensions of a given constraint interface. -Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans then it's useful to consider how they could be added in the future. +Relaxing the orphan rules is a backwards-compatible change. If this proposal is accepted without permitting orphans, it will be useful to consider how they could be added in the future. -Ideally we want to ban orphan implementations in libraries but not in executables; this allows a programmer to manually deal with coherence in their own code but prevents situations where adding a new library breaks code. +Ideally, we want to ban orphan implementations in libraries but not in executables; this allows a programmer to manually deal with coherence in their own code but prevents situations where adding a new library breaks code. ### Package-based approach to orphans -A simple way to allow orphan extenions is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages, it is possible to do the following. +A simple way to allow orphan extensions is to replace the file-based restrictions with package-based restrictions. Because there are no restrictions on packages, it is possible to do the following: ```kotlin // In some library foo @@ -560,7 +563,7 @@ extension object : Repository { } ``` -This approach would not forbid orphan extensions in libraries but it would highly discourage libraries from providing them, as this would involve writing code in the package namespace of another library. +This approach wouldn't forbid orphan extensions in libraries but, it would highly discourage libraries from providing them, as this would involve writing code in the package namespace of another library. ### Internal modifier-based approach to orphans @@ -580,7 +583,7 @@ The first problem actually leads us to a better solution. ### Java 9 module-based approach to orphans -Kotlin does not currently make use of Java 9 modules but it is easy to see how they could eventually replace Kotlin's `internal` modifier. The rules for this approach would be the same as the `internal`-based approach; code which uses orphans is not allowed to be exported. +Currently, Kotlin doesn't make use of Java 9 modules but, it's easy to see how they could eventually replace Kotlin's `internal` modifier. The rules for this approach would be the same as the `internal`-based approach; code which uses orphans is not allowed to be exported. ## Footnotes From e863b25f8b3f2e9b9aaac361c6ee52be31453ee0 Mon Sep 17 00:00:00 2001 From: Bloder Date: Thu, 18 Apr 2019 07:05:06 -0300 Subject: [PATCH 71/73] Move Group data class to GroupRepository (#15) As proposed on PR https://github.com/Kotlin/KEEP/pull/87 I think it makes sense to move `Group` class to the example that it's used, what do you think? --- proposals/compile-time-dependency-resolution.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index a7d72d095..41474e24c 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -141,8 +141,6 @@ interface Validator { In this scenario, the following classes would be equivalent: ```kotlin -data class Group(val values: List) - // Kotlin + KEEP-87 extension class ValidatedRepository(with val V: Validator) : Repository { @@ -195,7 +193,8 @@ package com.data.instances import com.data.Repository import com.domain.User -import com.domain.Group + +data class Group(val values: List) extension class GroupRepository(with val repoA: Repository) : Repository> { override fun loadAll(): Map> { From d488d79d2f733976a8b89bb54debb2cd95c6cf90 Mon Sep 17 00:00:00 2001 From: Jacob Bass Date: Mon, 6 May 2019 01:10:30 +1000 Subject: [PATCH 72/73] Update compile-time-dependency-resolution.md add an example of how to apply an extension to a function using the bound variable name. this is useful for when a function has some members with default values that may not be supplied on invocation, but the user still wants to supply an extension. --- proposals/compile-time-dependency-resolution.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index 41474e24c..e1b078bce 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -128,6 +128,7 @@ All values can be passed to functions with extension parameters, or we can omit ```kotlin fetchById(11829) // compiles since we got evidence of a `Repository` in scope. fetchById(11829, UserRepository()) // you can provide it manually. +fetchById(11829, repository=UserRepository()) // you can provide it manually with named application. ``` When `with` is used in class constructors, it is important to **add val to extension class fields** to make sure they are accessible in the scope of the class. Here, the `with` keyword adds the value to the scope of every method in the class. To showcase this, let's say we have a `Validator`, like: From da6a2744f343927d90682d0ecafd223bf3755e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Fri, 31 May 2019 18:38:55 +0200 Subject: [PATCH 73/73] Update proposals/compile-time-dependency-resolution.md Co-Authored-By: Pablo Gonzalez Alonso --- proposals/compile-time-dependency-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/compile-time-dependency-resolution.md b/proposals/compile-time-dependency-resolution.md index e1b078bce..1934a48eb 100644 --- a/proposals/compile-time-dependency-resolution.md +++ b/proposals/compile-time-dependency-resolution.md @@ -384,7 +384,7 @@ Here we got a `Repository` defined in a subpackage of `com.domain`, where #### 5. Subpackages of the package where the constraint interface is defined -The last place to look would be the subpackages of the package where the constraint interface is defined. t will simply look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. +The last place to look would be the subpackages of the package where the constraint interface is defined. It will simply look in subpackages under the current gradle module, it doesn't support cross-module definitions. These extensions **must be flagged as `internal`**. ```kotlin package com.data.instances