Skip to content
Closed
Changes from 9 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
13df9dd
Type Classes via natural extensions in Kotlin
raulraja Oct 2, 2017
7494c84
Adapted code examples to new proposed syntax https://github.com/Kotli…
raulraja Oct 2, 2017
a5f9659
Fixed typo
raulraja Oct 3, 2017
de8032b
Included section on overcoming `inline` `reified` limitations as show…
raulraja Oct 4, 2017
47e1348
Adapt proposal examples to new style using `given`
raulraja Oct 9, 2017
eb4f687
replace `extension class` for `extension object` where possible addre…
raulraja Oct 10, 2017
3777fa6
added imports to code examples
raulraja Oct 10, 2017
374e848
Add language changes and instance resolution rules order
raulraja Oct 12, 2017
2f2f27e
fixed wrong type param reference
raulraja Oct 24, 2017
3b568bf
code review comments on `most` vs `immediately`
raulraja Nov 7, 2017
7d10901
Reverted to use `typeclass` and `instance`
raulraja Nov 7, 2017
f8dce43
Added sentence clarifying the Reified example.
raulraja Nov 12, 2017
323a550
`Monoid.empty` is a value
fvasco Feb 19, 2018
a105496
Use IntMonoind in the example
fvasco Feb 27, 2018
505619c
Specify package in 'with'
fvasco Feb 27, 2018
115a483
Remove extra import in example 4
fvasco Feb 27, 2018
8a7dd15
Merge pull request #4 from fvasco/import4
raulraja Apr 9, 2018
8a74e37
Merge pull request #1 from fvasco/empty-value
raulraja Apr 9, 2018
20976f6
Merge branch 'master' into IntMonoind_empty
raulraja Apr 9, 2018
73b5d33
Merge pull request #2 from fvasco/IntMonoind_empty
raulraja Apr 9, 2018
7027bde
Merge pull request #3 from fvasco/intext_package
raulraja Apr 9, 2018
4a98c86
Using extension keyword
fvasco Apr 10, 2018
0496c73
Fix lambda declaration
fvasco Apr 11, 2018
da913fe
Compile resolution rules #4 clarification
fvasco Apr 11, 2018
6a10e93
Merge pull request #5 from fvasco/extension-interface
raulraja Apr 11, 2018
00d6aca
Update type class KEEP
Jun 25, 2018
d47eeef
Merge pull request #6 from ClaireNeveu/master
raulraja Jun 28, 2018
f495563
Update type-classes.md
raulraja Jun 28, 2018
d70f40c
Remove out-of-date section.
Jun 28, 2018
23b5276
Merge pull request #7 from ClaireNeveu/master
raulraja Jun 28, 2018
4671389
anonymous parameter clarification (#8)
fvasco Jun 29, 2018
84d16ce
Update proposal based on the initial implementation (#10)
truizlop Nov 14, 2018
b632229
Fix misuse of encoding (#9)
pakoito Nov 14, 2018
2a8b95a
Linguistic improvements within type-classes.md (#11)
TAGC Nov 15, 2018
3b74240
Fix typo: The intro… allow -> The intro… allows (#12)
LouisCAD Apr 10, 2019
5f7b61c
Updates contributors list.
JorgeCastilloPrz Apr 11, 2019
ce47c2e
Renames keep file.
JorgeCastilloPrz Apr 11, 2019
2417af2
First pass on Summary and Motivation.
JorgeCastilloPrz Apr 11, 2019
34d89c8
Description first pass.
JorgeCastilloPrz Apr 11, 2019
97f772f
Pass over inline reified section.
JorgeCastilloPrz Apr 11, 2019
c6835ea
Pass over composition and chain evidences section.
JorgeCastilloPrz Apr 11, 2019
9b6c6fd
Pass over Language changes section.
JorgeCastilloPrz Apr 11, 2019
f4ba65a
Polish Language changes a bit.
JorgeCastilloPrz Apr 11, 2019
ae5c0a7
Review resolution order.
JorgeCastilloPrz Apr 11, 2019
8c0737a
Remove type constructors section.
JorgeCastilloPrz Apr 11, 2019
eff56ad
Remove reified and inline generics section.
JorgeCastilloPrz Apr 11, 2019
33f55fc
Improve resolution order and add error reporting.
JorgeCastilloPrz Apr 11, 2019
988c8a9
Adds how to try sections.
JorgeCastilloPrz Apr 11, 2019
94a1a99
Polish how to test sections.
JorgeCastilloPrz Apr 11, 2019
bcbd746
Ploishes Orphan instances description a bit.
JorgeCastilloPrz Apr 11, 2019
dfc0b64
Completes first pass.
JorgeCastilloPrz Apr 11, 2019
a4fd7f4
Adds more details to some sections.
JorgeCastilloPrz Apr 11, 2019
a32eaa5
Adds some details to description.
JorgeCastilloPrz Apr 11, 2019
c811a73
Moar polishments.
JorgeCastilloPrz Apr 11, 2019
e6ba5e6
Fixes a typo.
JorgeCastilloPrz Apr 11, 2019
5485db1
Fixes a typo.
JorgeCastilloPrz Apr 11, 2019
ed8561e
Fixes a typo on a snippet.
JorgeCastilloPrz Apr 11, 2019
535c453
Includes horizontal composition in Summary.
JorgeCastilloPrz Apr 12, 2019
fba1db3
Updates repo to use Map property and adds A.save() extension.
JorgeCastilloPrz Apr 12, 2019
c563312
Update proposals/compile-time-dependency-resolution.md
raulraja Apr 12, 2019
21aeaed
swap How to try approaches.
JorgeCastilloPrz Apr 12, 2019
3dd43f5
Merge branch 'jc-keep-rewording' of github.com:47deg/KEEP into jc-kee…
JorgeCastilloPrz Apr 12, 2019
c8bf869
Fixes a typo in one of the snippets where a monoid was wrongly mentio…
JorgeCastilloPrz Apr 12, 2019
7792dd7
Adds proper package to User definition snippet.
JorgeCastilloPrz Apr 12, 2019
67c8f3e
Polishes wording over resolution order.
JorgeCastilloPrz Apr 12, 2019
6ba2577
Drops implicits from comparison.
JorgeCastilloPrz Apr 12, 2019
87d1c43
Fix wrong typing in loadById call sites.
JorgeCastilloPrz Apr 12, 2019
531711d
Rethink wording about named extensions.
JorgeCastilloPrz Apr 12, 2019
bd1416d
Rethink wording about named extensions.
JorgeCastilloPrz Apr 12, 2019
7bed642
fix conflicts.
JorgeCastilloPrz Apr 12, 2019
87eafe6
Remove FP constructs mentions from Error reporting screenshots.
JorgeCastilloPrz Apr 12, 2019
3753301
Refactor keep to reflect proper wording on first resolution scope.
JorgeCastilloPrz Apr 12, 2019
951ea3d
Reflect how syntax becomes available in methods and functions bodies.
JorgeCastilloPrz Apr 12, 2019
297e65f
Adds fun and val extension providers to the TODO list.
JorgeCastilloPrz Apr 15, 2019
071d6b7
Adds both big still open issues to the KEEP.
JorgeCastilloPrz Apr 15, 2019
6ac0d48
Uploads and links fixed Keep87Sample zip project using proper interna…
JorgeCastilloPrz Apr 15, 2019
e564b61
Adds clarification for internal modifier requirement in some scopes.
JorgeCastilloPrz Apr 15, 2019
21003c6
Switches chained to nested wording.
JorgeCastilloPrz Apr 15, 2019
83a1a94
Adds internal flag to required snippet extensions.
JorgeCastilloPrz Apr 15, 2019
abcd547
small lang changes
MaureenElsberry Apr 16, 2019
46e6a34
Merge pull request #14 from 47deg/jc-keep-rewording
JorgeCastilloPrz Apr 17, 2019
e863b25
Move Group data class to GroupRepository (#15)
bloderxd Apr 18, 2019
d488d79
Update compile-time-dependency-resolution.md
bassjacob May 5, 2019
78f5459
Merge pull request #16 from bassjacob/patch-1
JorgeCastilloPrz May 5, 2019
da6a274
Update proposals/compile-time-dependency-resolution.md
raulraja May 31, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 298 additions & 0 deletions proposals/type-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
# Type classes

* **Type**: Design proposal
* **Author**: Raul Raja
* **Status**: New
* **Prototype**: -

## 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`.

## Motivation

* 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 type classes and their instances with the same style interfaces are defined

```kotlin
interface Monoid<A> {
fun A.combine(b: A): A
fun empty(): A
}
```

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<Int>` instance that enables `combine` and `empty` on `Int`

```kotlin
package intext

object IntMonoid : Monoid<Int> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's risky to treat just any object as a type class instance, I think such objects should be marked. Probably, type class interfaces should be marked too. Also have a look at this paper: http://www.cs.cornell.edu/~ross/publications/shapes/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abreslav thanks for the link, I'll check it out. It was an original part of the discussion that only objects or classes flagged as extension should be suitable candidates to become type class instances available for resolution. I agreed that those should be specially flagged to avoid abuse and confusion with regular sub-typing. I'll include that in the proposal.

fun Int.combine(b: Int): Int = this + b
fun empty(): Int = 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty should be a value not a function.

}
```

```
import intext.IntMonoid

1.combine(2) // 3
Int.empty() // 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty() isn't an extension function of Int.Companion, this line should be replaced by IntMonoind.empty().

```

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 <A> add(a: A, b: A): A given 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
interface Context<A> {
fun A.config(): Config
}
```

```kotlin
package prod

object ProdContext: Context<Service> {
fun Service.config(): Config = ProdConfig
}
```

```kotlin
package test

object TestContext: Context<Service> {
fun Service.config(): Config = TestConfig
}
```

```kotlin
package prod

service.config() // ProdConfig
```

```kotlin
package test

service.config() // TestConfig
```

## Overcoming `inline` + `reified` limitations

Type classes allow us to workaround `inline` `reified` generics and their limitations and express those as type classes instead:

```kotlin
interface Reified<A> {
val selfClass: KClass<A>
}
```

Now a function that was doing something like:

```kotlin
inline fun <reified A> foo() { .... A::class ... }
```

can be replaced with:

```kotlin
fun <A> fooTC(): Klass<A> given Reified<A> { .... A.selfClass ... }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you imply that all classes will have to implicitly declare such Reified<Self> instances? This doesn't seem straighforwardly expressible as such, most likely those have to be additional classes, or be hardcoded. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a particular example of cases where you know in a inline reified context the concrete type params you want to support and then you can provide instances of Reified for those but it does not replace inline reified generics instrospected at runtime. that section originated from this comment #87 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, it's not clear from the proposal, how the instances are constructed, and the text suggests that it's ratehr a repacement than a complement. I think it needs to be clarified

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Scala the compiler injects an instance in the actual argument default value because they use implicit arguments but in Kotlin given this was related to extensions the compiler will enable the extension syntax provided by the typeclass in the same way extensions functions work when brought into scope. At the end of the day the compiler needs to inject bytecode that creates a new object if it's a class or uses the singleton value if an object similar to how with(instance) { } works now. Open to ideas as to how this should better work :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a load of open questions:

  • where is the Reified interface defined?
  • what's its exact API?
  • how many instances need being created and how they are cached?
  • do new classes need to be created to instantiate Reified (a positive answer to this sounds like a show stopper to me)?

Copy link
Author

@raulraja raulraja Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a load of open questions:
where is the Reified interface defined?
what's its exact API?

I would not use Reified as a type class but was an example provided by @elizarov as having similar semantics.
A type class interface may be defined anywhere in user or library code such as

package x
extension interface Reified<A> {
  fun A.kclass(): KClass<A>
}
package y
extension object StringReified : Reified<String> {
  fun String.kclass(): KClass<A> = String::class
}
package z

import y.*

fun <A> getKClass(): KClass<A> given Reified<A> = A.kclass()

object test {
  val sc = getKClass<String>() //compiles
  val ic = getKclass<Int>() // does not compile
}

how many instances need being created and how they are cached?

In the case of using an object the instance itself is the value. In the case of defining instances that declare further evidence of other type classes in terms of a class:

extension class OptionMonoid<A>: Monoid<Option<A>> given Monoid<A>

The compiler may need to instantiate it if we allow for mutable state or just cache the instance.

do new classes need to be created to instantiate Reified (a positive answer to this sounds like a show stopper to me)?

I don't think so because the user declared instances themselves are the evidence. We restricted in the proposal usage to just object and class but allowing val or fun providers is also an option for resolution, though that looks a lot more than Scala implicits and the purpose is that injection and resolution would be restricted to type classes and all of those are parametric to at least one type param.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reified

This suggests that the instance has to be defined manually every time: an instance for String, instance for Int, and so on. This can't be practical, TBH. And if you expect the compiler to automatically define these, there has to be a caching mechanism or something described in this proposal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler may need to instantiate it if we allow for mutable state or just cache the instance.

There's no way in the language to guarantee purity/immutability, so the compiler has to assume mutable state most of the time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We restricted in the proposal usage to just object and class but allowing val or fun providers is also an option for resolution

In terms of resolution, there's not much of a difference between object and val

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reified is not a good example for type classes when the use case is instrospection of reified runtime generics with reflection.
Coupling behavior to data types without inheritance that are extensible by users ad-hoc where needed is the killer feature.
Perhaps I should remove the Reified example from the proposal so it does not lead to confusion as it is not a real replacement when you want to do runtime reflection generalized to all types.

```

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<A> {
val someKlazz = foo<A>() //won't compile because class disallow reified type args.
}
```

```kotlin
class Foo<A> {
val someKlazz = fooTC<A>() //works anddoes not requires to be inside an `inline reified` context.
}
```

## 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
package optionext

class OptionMonoid<A> : Monoid<Option<A>> given Monoid<A> {

fun empty(): Option<A> = None

fun Option.combine(ob: Option<A>): Option<A> =
when (this) {
is Some<A> -> when (ob) {
is Some<A> -> Some(this.value.combine(b.value)) //works because there is evidence of a Monoid<A>
is None -> ob
}
is None -> this
}

}
```

The above instance declares a `Monoid<Option<A>>` as long as there is a `Monoid<A>` 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<Option<A>>` instance providing `combine` but no `Monoid<String>` 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
interface FunctionK<F<_>, G<_>> {
fun <A> invoke(fa: F<A>): G<A>
}

object Option2List : FunctionK<Option, List> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like higher kinds are not needed here. Above you imply that a class can serve as a "factory" of type class instances (see OptionMonoid), so here we could do the same:

(syntax is just schematic)

/*type class*/ interface FunctionK<F, T> {
    fun invoke(fa: F): T
}

/*type class instance*/ class OptionToList<A> : FunctionK<Option<A>, List<A>> {
    fun invoke(fa: Option<A>): List<A> = ...
}

In the Functor case we'd need to use a trick to avoid higher kinds, but it's doable. I know of two options.

Define an Arg typeclass to extract the type argument, requires all subject types to cooperate:

typeclass interface Arg<A> {} // Type Argument #0
typeclass interface Functor<A, B> given Arg<A>, Arg<B> {
    fun map(a: A, f: (Arg<A>) -> Arg<B>): B
}

// Here I assume that `given F : Functor<A, B>` defines a new type variable
fun <A, B> transform(f: (Arg<A>) -> Arg<B>): B given F : Functor<A, B> = F.map(fa, f)

Then the client code would be:

class Option2List<A, B> : Functor<Option<A>, List<B>> {
    override fun map(a: Option<A>, f: (A) -> B): List<B> = ...
}

// where subjects must be defined as
class List<A> : Arg<A>, Collection<A> { ... }
clas Option<A> : Arg<A>, ... { ... }

Or, Functor can itself define the args:

typeclass interface Functor<ArgA, ArgB, A, B> {
    fun map(a: A, f: (ArgA) -> ArgB): B
}

fun <ArgA, ArgB, A, B> transform(f: (ArgA) -> ArgB): B given F : Functor<ArgA, ArgB, A, B> = F.map(fa, f)

class Option2List<A, B> : Functor<A, B, Option<A>, List<B>> {
    override fun map(a: Option<A>, f: (A) -> B): List<B> = ...
}

In any case, higher kinded types seem like an overkill here

Copy link
Author

@raulraja raulraja Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of Functor one needs to be able to go from F<A> -> F<B> but that case goes from F<A> -> G<A>. One needs the guarantee that those functions return the same container F<_>. We currently encode higher kinded parametric typeclasses like this https://github.com/kategory/kategory/blob/master/kategory-core/src/main/kotlin/kategory/typeclasses/Functor.kt

This uses the HK<F, A> trick to emulate kinds. which requires later safe downcasts via ev() to get from a HK<OptionHK, A> -> Option<A> where OptionHK is an extra marker interface and just boilerplate. This is actually what our users complain the most. Also I believe most useful type classes in the domain of FP are higher kinded and many others in OOP can be too. For example write once the Factory of Factories for all Repository<A> types that adhere to Creatable where A is a model class

The biggest issue with client code doing something like class List<A> : Arg<A> is that when you write type class instances you actually are doing it over types you can't change. List being one of them since it's in the std lib.

The big feature here is to add behaviors to types you have not written yourself without subtyping just by evidence of imports in scope. For example type classes in Kotlin would allow me to add a Functor instance for Deferred automatically activating:

async { doStuff }.map { .. }

Now Kotlin Coroutines can become a first class citizen in apps that use Kategory and we can automatically prove laws and provide not just useful combinators but to write polymorphic programs that are independent of the data type: Another example Async

suspend fun <F<_>, A> bindAsync(fa: F<A>): A given Async<F> = ...
class DeferredAsync : Async<Deferred> {}
class CompletableFutureAsync : Async<CompletableFuture> {}

bindAsync(deferred) // works
bindAsync(future) // works

We can effectively abstract away the data types and code to behaviors as you do with interfaces today but no need for subtyping or inheritance in the mix. Also the compiler checks if the dependencies are not in scope. IMO without Higher Kinds we increase the complexity of the encoding and the burden on the user of further passing context around as type args to achieve parametrization over a container type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to point out that we are usually considering primarily the use cases outside of traditional paradigms (like FP), and more around pragmatic use cases, so we'll need to work on these

Copy link

@d3xter d3xter Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classic example where HKT would've been useful are the higher order functions on Iterables (e.g. map, filter).

Right now, it doesn't matter what type of Collection you have, you'll always get ArrayList<R> back (or use mapTo).

Copy link
Author

@raulraja raulraja Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. I find type classes useful in all contexts. For example here is something library authors can provide with type classes and kinds both in OOP and FP.

Allow users to choose their own return type:

There are libs that use Future<A>, others use Deferred<A> others use CompletableFuture<A>, and the list of runtime possible async containers goes on and on. The lib author can write its libs in terms of Async and provide a method such as that:

fun <F<_>, A> run(f: () -> A): F<A> given Async<F> = F.runBlock(f)

The lib author can now provide in separate modules lib-deferred lib-futures the following instances in their modules respectively:

extension class AsyncDeferred<A>: Async<A> { ... }
extension class CompletableFuture<A>: Async<A> { ... }

Now users can choose how to run their programs because they are parametric and can choose their own return types which works with their current stack. Some users may import the runtime for CompletableFuture and others the one for Coroutines.
If a user was working for example with AsyncTask on Android he could write his own instance of Async<AsyncTask> and use the library. Type classes open the option to do ad-hoc polymorphism.

A similar use case is DI, Encoder/Decoders, and the list goes on, all of them verified at compile time for all paradigms.

fun <A> invoke(fa: Option<A>): List<A> =
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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should F<_> be F<*>?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a comment below, on the main thread, as to why IMHO we should avoid using Scala-like syntax for Higher-Kind Type Parameters.


A use of this declaration in a polymorphic function would look like:

```kotlin
fun <F<_>, A, B> transform(fa: F<A>, f: (A) -> B): F<B> given 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<_>
transform(listOf(1), { it + 1 }) // does not compile: No `Functor<List>` instance defined in scope.
```

## 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<A> : Monoid<Option<A>> given Monoid<A> //class position

fun <A> add(a: A, b: A): A given Monoid<A> = 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<A>(instance MA: Monoid<A>) : Monoid<Option<A>> //class position

fun <A> add(a: A, b: A, instance MA: Monoid<A>): A = a.combine(b) //function position
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between named and unnamed variants? Are these two options to choose between?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ilya-g
the initial idea is to name "Monoid" the Monoid argument, now I consider this pretty messy.
"Monoid" is the argument name, the parameter class name or the Monoid typeclass.

I consider better treat is as anonymous declaration, so

fun <A> add(a: A, b: A, with Monoid<A>): A

is same as

fun <A> add(a: A, b: A, with _: Monoid<A>): A

In such case it is possible to use a type class easy, however if you want to reference to it then you have to declare explicilty its name.


## 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<A>` is in scope.
```kotlin
fun <A> add(a: A, b: A): A given Monoid<A> = a.combine(b) //compiles
```
- The type checker will declare the below definition as invalid since there is no `Monoid<Int>` in scope.
```kotlin
add(1, 2)
```
- The type checker will declare the below definition as valid since there is a `Monoid<Int>` in scope.
```kotlin
import intext.IntMonoid
add(1, 2)
```
- The type checker will declare the below definition as valid since there is a `Monoid<Int>` in scope.
```kotlin
fun addInts(a: Int, b: Int): Int given 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(IntMonoid) { add(a, b) }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example requires a missing import, so it should be replaced by:

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:

Declaration:
```kotlin
fun <A> add(a: A, b: A): A given Monoid<A> = 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<Int>` exists in scope.

1. Look in the most immediate scope for declarations of `given Monoid<Int>` in this case the function `addInts`

This will compile because the responsibility of providing `Monoid<Int>` is passed unto the callers of `addInts()`:
```kotlin
class AddingInts {
fun addInts(): Int given Monoid<Int> = add(1, 2)
}
```

2. Look in the most outher class/interface scope for declarations of `given Monoid<Int>` in this case the class `AddingInts`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "most outer" and not immediately outer?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

immediately outer is the right way, I'll change that, thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, there's an entire lookup loop there, and you need to figure out all kinds of containers, including classes that inherit instantiations from supertypes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a question of whether a val can work as evidence for a type class

```kotlin
class AddingInts given Monoid<Int> {
fun addInts(): Int = add(1, 2)
}
```
This will compile because the responsibility of providing `Monoid<Int>` is passed unto the callers of `AddingInts()`

3. Look in the import declarations for an explicitly imported instance that satisfies the constrain `Monoid<Int>`:
```kotlin
import intext.IntMonoid
class AddingInts {
fun addInts(): Int = add(1, 2)
}
```
This will compile because the responsibility of providing `Monoid<Int>` is satisfied by `import intext.IntMonoid`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, no *-imports? People may not be happy with it.

I'd suspect that we'll need to think in advance of a strategy for code completion for such functions: where does the IDE look for instances and what does it import if needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like * imports. The only issue with * imports is that if the compiler found ambiguous instances it should bail with a proper message. It was suggested at some point that those so be explicit so that newbies were not confused as to where the instances where getting applied from but if we want to support import kategory.* for example to bring all of our instance into scope that would be awesome. The IDE is doing something similar in the Scala plugin in the case of implicits. It looks in all symbols imported in a given scope trying to find candidates for resolution and it if it finds one it activates the syntax.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*-imports will likely pose a performance challenge for the compiler

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something worth mentioning is that instances are only resolved at call sites where the invocation is concrete so the compiler does not need to look into all * where they are declared just where functions are invoked. I believe is the same way resolution now works to bring other symbols into scope without fully qualifying them with the full package name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just where functions are invoked.

It's very-very many places in the code :)

I believe is the same way resolution now works to bring other symbols into scope without fully qualifying them with the full package name.

Not quite. Other symbols are bound by name, here we are binding by type, and it's a lot more work for the compiler

Copy link

@JLLeitschuh JLLeitschuh Jun 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about cases where you want to import two implementations of the Monoid<Int> into the scope of one file?

Do you have to resolve this conflict by calling each scoped by a with block??

import intext.IntMonoid1
import intext.IntMonoid2

fun addInts1(a: Int, b: Int): Int = with(IntMonoid1) { add(a, b) }
fun addInts2(a: Int, b: Int): Int = with(IntMonoid2) { add(a, b) }

Edit:
NVM: This is defined below

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example 4 is the same of 3, should you remove this import?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fvasco feel free to submit a PR with those changes against our fork and we can get them integrated in the proposal.

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<Int>` 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