New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type Classes for Kotlin #87

Open
wants to merge 34 commits into
base: master
from

Conversation

@raulraja
Copy link

raulraja commented Oct 2, 2017

The following PR adds a KEEP proposing a natural fit for Type Classes and higher kinded types in the Kotlin's extensions mechanism.

Current status: https://github.com/47deg/KEEP/blob/master/proposals/type-classes.md
Working POC thanks to @truizlop with instructions to run it arrow-kt/kotlin#6.

Want to help to bring Type Classes and HKTs to Kotlin?. A fork is being provisioned where a reference implementation based on this proposal will take place at https://github.com/arrow-kt/kotlin

@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 2, 2017

The proposed syntax for companion constrains looks a bit confusing. Take a look at this example:

typeclass Monoid {
    fun combine(b: Self): Self 
    fun Self.Companion.empty(): Self
}

It is not perfectly clear from this declaration what is the receiver type of combine and empty functions. It is, in fact, Self for combine and Self.Companion for empty, but the syntax is not regular enough to make it clear.

I was thinking a bit on how to fit companion-extension syntax in a nicer way and came up with the following alternative syntactic approach.

Let's eschew the idea that extension declaration is a way to group multiple extension functions together, but instead leave the regular extension function syntax intact inside both typeclass and extension scopes like this:

typeclass Monoid {
    fun Self.combine(b: Self): Self
    fun Companion.empty(): Self
}

Here, in the context of typeclass declaration (and only there) we treat Self and Companion as context-sensitive keywords that refer, respectively, to the actual class that this typeclass applies to and its companion (regardless of how its companion is in fact named). Now, when declaring the actual extension we'll have:

extension Int : Monoid {
    fun Int.combine(b: Int): Int = this + b
    fun Int.Companion.empty(): Int = 0
}

In this approach extension block serves just a wrapper and an additional safety net. It does not bring any scope with it. All the functions declared inside extension are going to be treated normally (as if they were on the top level), but compiler will complain if they do not match the signatures of the corresponding type-class.

Unfortunately, this proposal also opens the whole can of worms on companion-vs-static debate. One of the issues being the fact, that not all classes in Kotlin (and even in Kotlin stdlib) have companions and there is currently no way to declare a companion outside of the class. Moreover, it is hardly possible to fix or to work around due to dynamic nature of JVM.

One solution to this conundrum is to stop piggybacking onto companion objects at all. Let us, instead, declare functions on the typeclass itself, like this:

typeclass Monoid {
    fun Self.combine(b: Self): Self
    fun empty(): Self
}

In this approach typeclass does declare a scope in a similar way to interface declaration. We interpret combine as a function with two receivers (an instance of the typeclass and Self), while empty has only one receiver (an instance of typeclass). In fact, it meshes perfectly with how the typeclasses are going to be represented on JVM anyway. Now the extension declaration is going to look like this:

extension Int : Monoid {
    fun Int.combine(b: Int): Int = this + b
    fun empty(): Int = 0
}

The interesting thought direction here is that this kind of extension mechanism can work as a replacement for companion objects altogether. We might just deprecate companions, because any declaration of the form:

class Foo {
    companion object {
       /** body **/
    }
}

can be replaced, for the same effect, with

class Foo {
    extension Foo {
       /** body **/
    }
}

or, which is not possible with companion objects, if the access to private members of Foo is not needed, it can be moved outside:

class Foo {
}

extension Foo {
    /** body **/
}

It is cleaner and more modular, since you are not limited to one companion object anymore, but can define as many as you want.

However, with this alternative-to-companion approach to typeclasses we have to work out on how the typeclass instances are going to be named and what is the syntactic approach to explicitly specify this name (this is, at least, needed for JVM interop). We can also try to somehow reuse companion modifier for extension blocks by imbuing it with an additional syntax. Lots of things to think about.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 2, 2017

@elizarov makes perfect sense. I'll update the proposal to the new proposed syntax

}
```

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

This comment has been minimized.

@mkobit

mkobit Oct 2, 2017

Small note seeing this link, you may want to copy/paste the snippets if you believe it provides important context because the Kotlin slack is free and only has the most recent 10k messages.

This comment has been minimized.

@raulraja

raulraja Oct 2, 2017

@mkobit thanks, I copied most of the snippets from @elizarov into the proposal so I think we are good if that goes away.

@mikehearn

This comment has been minimized.

Copy link

mikehearn commented Oct 2, 2017

I realise all this stuff comes from Haskell and people who love category theory, but I strongly suggest that to gain better acceptance you drop the conventional FP names for things and use more appropriate names that are less jargony and more Java-like:

Monoid -> Combinable
Functor -> Mappable

etc

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 2, 2017

@mikehearn those are just examples and not part of the proposal. Anyone is free to name their own typeclasses whatever they want and this proposal does not address what names users will choose nor it proposes new types for the stdlib.
Also the examples don't come from Haskell. Those names and type classes I used as examples are part of many libraries and langs by now including libs in Kotlin.

@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 3, 2017

How type class implementations (extensions) are resolved?
When two implementations of the type classes are in conflict?

Elaborating on the example with ProdConfig and TestConfig, suppose I have

package monoid

typeclass Monoid {
    fun Self.combine(b: Self): Self
    fun empty(): Self
}

and, for example,

package org.acme

extension Int : Monoid {
    fun Int.combine(b: Int): Int = this + b
    fun empty(): Int = 0
}

and

package org.fizzbuzz

extension Int : Monoid {
    fun Int.combine(b: Int): Int = this * b
    fun empty(): Int = 1
}

Now, in package myapp, I want to use one particular implementation of Monoid.
What should I import?

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 3, 2017

@dnpetrov an import matching the contained instances would bring them into scope:

import org.fizzbuzz.*

In the case where you have two instances that are conflicting the compiler should bail because they are ambiguous and the user needs to disambiguate them.

import org.fizzbuzz.*
import org.acme.*

val x: Int = 1.combine(2) // // Ambiguous instances for Monoid: Int. Please redefine`extension Monoid: Int` in the current scope or use tagged instances if they provide different behavior.

A natural way for disambiguation may be redefining the instance in the current scope which would take precedence over the imported one.
If the compiler generated synthetic names for the instances or supported users naming them you could just import the FQN.

import org.fizzbuzz.monoidIntAdd 

Another approach would be to place further constrains in the instances like is done today with the <A: Any> trick where it is used to ensure non nullable values are always passed as args to functions.

typeclass Add : Int
typeclass Multiply : Int

extension Int : Monoid : Add {
    fun Int.combine(b: Int): Int = this + b
    fun empty(): Int = 0
}

extension Int : Monoid : Multiply {
    fun Int.combine(b: Int): Int = this * b
    fun empty(): Int = 1
}

fun <Tag, A: Monoid: Tag> combine(a: A, b: A): A = a.combine(b)

combine<Add, Int>(1, 1) // 2
combine<Multiply, Int>(1, 1) // 1
combine(1, 1) // Ambiguous instances for `Monoid: Int`. Please redefine`extension Monoid: Int` in the current scope or use tagged instances if they provide different behavior.
@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 3, 2017

Let's assume the example by @dnpetrov

package org.acme

extension Int : Monoid {
    fun Int.combine(b: Int): Int = this + b
    fun empty(): Int = 0
}

It is pretty straightforward with star imports:

import org.acme.*

To disambiguate extension in the case of start import from the other package, I suggest to use the simple name of the class that extension is being defined for, like this:

import org.acme.Int

This will bring in scope all Int extensions defined in this package.

This approach has a nice and quite well-intended side-effect that if a class and its extension are both defined in the same package, then a qualified import of a class automatically brings into scope all the extensions defined for this class in the same package. We can even go as far as to forbid redefining class extensions in other packages if they are defined in the original package. This will improve predictability and is quite consistent with a class member vs extension resolution in Kotlin (class member, being defined by the class author, always takes precedence).

However, if there is no extension in the class's original package, then different clients can define and/or import different extension instances for this class with the same typeclass.

Also note, that a generic method with typeclass upper-bounds should have an implict instance of typeclass in scope behind the scenes (actually passed to it by invoker) and this instance should always take precedence.

@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 3, 2017

The interesting side-effect of this proposal is that we can almost deprecate reified modifier and reimplement it with type-classes instead (not really):

typeclass Reified {
    val selfClass: KClass<Self>
}

Now a function that was doing something like:

fun <reified T> foo() { .... T::class ... }

can be replaced with:

fun <T : Reified> foo() { .... T.selfClass ... }

This analogy might help to understand how typeclasses shall behave in various scenarios. In particular, generic classes with typeclass upper bounds shall be supported if and only if we roll out support of generic classes with reified type parameters.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 3, 2017

@elizarov that will simplify most of our code a ton since we are currently relying on inline <reified A>... all over the place and we frequently find places inside generic classes where we can't use those methods with the classes type args.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 4, 2017

Updated the proposal with a section on overcoming some of the limitations of inline reified with Typeclass constrain evidences as demonstrated in #87 (comment)

@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 4, 2017

What if I have multiple type class implementations for the same type in the same package?
Like

package org.acme.bigint

class BigInt { ... }

extension BigInt : Monoid { ... }
extension BigInt : Show { ... }

I'd expect extensions to have names or something. Implicit imports look somewhat fragile. You don't know what else you bring into the scope.

Maybe even just follow familiar class inheritance syntax:

typeclass Monoid<T> {
    fun T.combine(b: T): T
    fun empty(): T
}

extension AdditiveInt : Monoid<Int> {
    override fun Int.combine(b: Int): Int = this + b
    override fun empty(): Int = 0
}

Then typeclass becomes just a special kind of interface, and extension - a special kind of object, with its members implicitly imported in file scope, and all regular resolution rules just work as they should. Note that you also get multi-parameter type classes for free.

@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 4, 2017

With parametric type classes, type class dependent functions might need some different syntax, like:

fun <T> combineWith(others: Iterable<T>): T where Monoid<T> { ... }
@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 4, 2017

@dnpetrov As for the syntax makes sense. That syntax is similar to what I originally proposed as well since it is also necessary to implement the fact that certain type classes can't target a single type but target the combinations of many. For example a natural transformation to go from one type to another:

typeclass FunctionK<F<_>, G<_>> {
  fun <A> invoke(fa: F<A>): G<A>
}

extension Option2List : FunctionK<Option, List> {
  fun <A> invoke(fa: Option<A>): List<A> =
    fa.fold({ emptyList() }, { listOf(it) })
}

In the case above the instance does not target neither Option nor List but those are part of the type args and folks should be able to summon the instance somehow if they can prove that such instance exists for both types. I'm not sure how that case could be expressed with extension syntax.

@dnpetrov In your example above an alternative way to express combineWith could be:

fun <T> combineWith(others: Iterable<T>, instance MT : Monoid<T>): T = ...

That would allow call sites to replace the instance by explicitly providing one if needed so the method could be invoked in a similar way as:

combineWith(others) //compiles if Monoid<T> instance is found and fails if not.
combineWith(others, customInstance) // always compiles

That also is an alternative way to handle ambiguity when collisions exists beside using concrete imports.

@elizarov @dnpetrov thoughts? Thanks!

@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 4, 2017

@raulraja Looks good. Need some time to reshuffle my thoughts and probably show this to the owner of Resolution & Inference. @erokhins pls take a look...

@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 4, 2017

The latest syntactic proposal by @dnpetrov for typeclass declaration is indeed more general and is easier to adapt to to the can of typeclasses binding multiple types.

However, I don't like the need to name typeclasses and more so the need to declare and name typeclass instances in function signature. It is just a boilerplate. IMHO, it defeats the whole purpose of having type-classes as a language feature, since you can already do all that.

At this point I would recommend to (re)read Cedric's blog post on ad-hoc polymorphism. In our example, we can already write just:

interface Monoid<T> {
    fun T.combine(b: T): T
    fun empty(): T
}

then write

object AdditiveInt : Monoid<Int> {
    override fun Int.combine(b: Int): Int = this + b
    override fun empty(): Int = 0
}

then declare functions with an explicit type class instance:

fun <T> combineWith(others: Iterable<T>, MT : Monoid<T>): T = ...

It is even shorter than the latest syntactic proposal for typeclasses, since I don't have to add instance modifier before MT. So, what is the value of the type classes proposal then?

IMHO, the only valuable benefit of having type classes directly supported in the language is making them easier to grasp for beginners by capitalizing on the mental model of interfaces on steroids (aka Swift protocols), and eschewing as much boilerplate as we can in the process. Having to name your type class instance is just a boilerplate, having to pass your type-class instances explicitly is just a boilerplate. Neither should be required.

@cbeust

This comment has been minimized.

Copy link
Contributor

cbeust commented Oct 4, 2017

A nitpick but in the examples, I think you should be more specific about the name of the Monoid types, e.g. MonoidAddition, MonoidMultiplication, etc...

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 4, 2017

@elizarov Kategory uses interfaces for typeclasses now and does it like that. The issue is that:

fun <T> combineWith(others: Iterable<T>, MT : Monoid<T>): T = ...

Requires explicitly passing the instance manually which defeats the purpose of type classes. Having a keyword to require the compiler to verify that call sites such as combineWith(listOf(1)) compiles because AdditiveInt is in scope is the real feature because we get compile time verification of constrains.

I agree though that the variant based on extensions is much easier to grasp for beginners. We can perhaps encode all complex cases where typeclasses require multiple types if typeclasses allowed for type args:

typeclass FunctionK<G<_>> {
  fun <A> Self<A>.convert(): G<A>
}

extension Option : FunctionK<List> {
  fun <A> Option<A>.convert(): List<A> =
    this.fold({ emptyList() }, { listOf(it) })
}

val x: List<Int> = Option(1).convert() // [1] 
val y: List<Int> = None.convert() // [ ] 

And in a polymorphic context:

fun <G<_>, F<_>: FunctionK<G>, A> convert(fa: F<A>): G<A> = fa.convert()
val l: List<Int> = convert(Option(1))
val s: Set<Int> = convert(Option(1)) // No `Option: FunctionK<Set>` extension found in scope.
@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 5, 2017

Having played more with @dnpetrov syntactic proposal I start to like certain parts of it. First of all, the generic-like declaration for interface indeed looks more concise than my original proposal with the Self keyword. Being able to eschew the "magic" nature of Self is also good.

Moreover, instead of a having to reserve a new typeclass keyword we can just be explicit about the fact that it just an interface, but tag this interface with an extension modifier like it is customary in Kotlin's syntactic tradition:

extension interface Monoid<T> {
    fun T.combine(b: T): T
    fun empty(): T
}

This syntax also scales to multiple types (and, maybe HKTs, but I will not cover them here). For example, you can have:

extension interface Bijection<A, B> {
    fun A.toB(): B
    fun B.toA(): A
}

However, as soon as you allow applying this syntax to multiple types, the story with "standalone" (static? but not really) functions like empty becomes complicated. We do want those functions to extend "the type" and we want them being applicable on the type itself (like companion object functions for the type). You should be able to use Int.empty() expression in the context of Monoid<Int>, but with multiple types it becomes unclear which type is being extended this way.

In order to overcome this difficulty, we can use some fresh syntactic construct to bind the type to its typeclass. Look at how the dot (.) can work for this purpose and how it is consistent with Kotlin's approach to extension functions:

extension interface T.Monoid {
    fun T.combine(b: T): T
    fun empty(): T
}

In this context, T will be called "an extension receiver type" (that is, the type that receives the extension).

Note, that we can allow to add additional types in <...> after the extension interface name and allow "receiver-less" extensions like Bijection, too.

Similarly, declaration of typeclass instances can ready follow the model of Kotlin's companion object. Name can be optional, too, so that an instance without a name can be written like this:

extension object : Int.Monoid {
    override fun Int.combine(b: Int): Int = this + b
    override fun empty(): Int = 0
}

Since we have indicated that Int is an "extension receiver type", it becomes clear and syntactically transparent on why one can write Int.empty() in the context of this extension object. It is also clear on how to provide an explicit name, if needed.

Alas, this notation makes it less obvious on how to specify typeclass constraints for functions. The interface-inspired syntax of T : Monoid does not look consistent with the declaration of Monoid anymore and breaks altogether with receiver-less type classes like Bijection<A,B>. I don't think that reusing a where clause works either, but we can introduce a new clause for this purpose, for example:

fun <T> combineWith(others: Iterable<T>): T given T.Monoid = ...
@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 5, 2017

@elizarov

However, as soon as you allow applying this syntax to multiple types, the story with "standalone" (static? but not really) functions like empty becomes complicated. We do want those functions to extend "the type" and we want them being applicable on the type itself (like companion object functions for the type). You should be able to use Int.empty() expression in the context of Monoid, but with multiple types it becomes unclear which type is being extended this way.

This approach has some issues with overloading. E.g., what if I have several different type classes (extension interfaces, whatever) for T with method empty()?
I'd suppose it should rather be more explicit, like in #87 (comment), where type class implementation is passes as a named parameter, which has special default value resolution strategy.

I'd expect something like:

extension interface Monoid<T> {
    fun T.combine(b: T): T
    fun empty(): T
}

extension object AdditiveInt : Monoid<Int> {
    override fun Int.combine(b: Int): Int = this + b
    override fun empty(): Int = 0
}

extension object StringConcatenation : Monoid<String> {
    override fun String.combine(b: String): String = this + b
    override fun empty(): String = ""
}

fun <T> T.timesN(n: Int, extension monoid: Monoid<T>): T {
    var result = empty() // 'monoid' is an implicit dispatch receiver
        // or, if you need to disambiguate, 'monoid.empty()'
    for (i in 0 until n) {
        result = result.combine(this) // 'monoid' is an implicit dispatch receiver, 'combine' is a member extension
          // or 'monoid.run { result.combine(this) }'
    }
    return result
}

fun test() {
    assertEquals("abcabcabcabc", "abc".timesN(4))
}
@dnpetrov

This comment has been minimized.

Copy link
Contributor

dnpetrov commented Oct 5, 2017

Now, when I look at my example above, it starts to remind me "extension providers" from Xtend (https://www.eclipse.org/xtend/documentation/202_xtend_classes_members.html#extension-methods).
Probably we'll also need 'extension val's.

@raulraja @elizarov
I think we need some more practical motivating example. Yeah, all those Monoids and BiFunctions and HKTs thrown in here and there are nice. Can we think of a concise, meaningful piece of code to demonstrate why type classes (extension interfaces) make life better? Like HTML builders for extension lambdas, for example.

@d3xter

This comment has been minimized.

Copy link

d3xter commented Oct 5, 2017

One possible motivation could be ad-hoc polymorphism as described by Cedric.
An Example is Limiting the possible values put into a JSON.

For example in klaxon to create a new Json-Object you have the following method:
fun obj(vararg args: Pair<String, *>): JsonObject Link

This basically allows everything to be put inside.
Other JSON-Libraries like json-simple have many methods called putString/putInt and so on.

With typeclasses we can define a JSONValue:

typeclass T.JSONValue {
    fun T.toJSON() = String
    fun fromJSON(String) = T
}

extension object : Int.JSONValue {
    fun Int.toJSON() = this.toString()
    fun fromJSON(a: String) = a.toInt()
}

Then all you need inside your JSON-Library is a method like that:

fun <T> JsonObject.put(key: String, value: T) given T.JSONValue { ... }

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 5, 2017

@dnpetrov here are a few:

Type classes cover many use cases, here are a few interesting ones syntax aside:

Compile-Time Dependency Injection

package common

extension interface Dependencies<T> {
    fun config(): Config
}
package prod

extension object ProdDeps: Dependencies<Service> {
    fun config(): Config = ProdConfig
}
package test

extension object TestDeps: Dependencies<Service> {
    fun config(): Config = TestConfig
}
import test.*

Service.config() // TestConfig
import prod.*

Service.config() // ProdConfig

In the example above we used config but you can apply a similar case to the Android Activity which managing it one of the biggest issues for developers.

Type restrictions for safer operations

extension interface Numeric<T> {
    fun T.plus(y: T): T
    fun T.minus(y: T): T
}

extension Fractional<T> : Numeric<T> {
    def T.div(y: T): T
}

extension object : Numeric<Int> {
    override fun Int.plus(y: Int): Int = this + y
    override fun Int.minus(y: Int): Int = this + y
}

extension object : Fractional<Double> {
    override fun Double.plus(y: Double): Double = this + y
    override fun Double.minus(y: Double): Double = this + y
    override fun Double.div(y: Double): Double = this / y
}

fun <T> add(x: T, y: T, extension N: Numeric<T>): T = x.plus(y)
fun <T> div(x: T, y: T, extension F: Fractional<T>): T = x.div(y)

add(1, 1) // 2
add(1.0, 1.0) // 2.0
div(3.0, 5.0) // 0.6
div(3, 5) // No `Fractional<Int>` instance found in scope.
//In Kotlin now 3/5 == 0 loosing precision.

Type safe composable encoder / decoders and reflectionless instrospection

Expanding on what @d3xter mentions in #87 (comment)

sealed class JValue {
    object JNull: JValue()
    class JString(s: String): JValue()
    class JDouble(num: Double): JValue()
    class JDecimal(num: BigDecimal): JValue()
    class JInt(num: Int): JValue()
    class JBool(value: Boolean): JValue()
    class JObject(obj: List<JField>): JValue()
    class JArray(arr: List<JValue>): JValue()
}

typealias JField = Pair<JString, JValue>

extension Writes<T> {
  fun writes(o: T): JValue
}

extension Reads<T> {
  fun reads(json: JValue): T
}

extension Format<T> : Reads<T>, Writes<T>

//Primitive to JValue instances omitted for simplicity

fun <T> T.toJson<T>(instance tjs: Writes<T>): JValue = tjs.writes(this)

fun <T> JValue.to(intance fjs: Reads<T>): T = fjs.reads(this)

data class Person(val name: String, val age: Int)

object extension Writes<Person> {
    fun writes(o: Person): JValue = JObject(listOf(
        "name".toJson() to o.name.toJson(), // fails to compile if no `Reads<String>` is defined
        "age".toJson() to o.age.toJson(), // fails to compile if no `Reads<Int>` is defined
    ))
}

Person("William Alvin Howard", 91).toJson() // if both `Reads<String>` `Reads<Int>` is defined : { "name": "William Alvin Howard", age: "91" }

The most important use case of them all is that users and library authors can write code with typeclasses and default instances providing complete solutions to their users. At the same time users have the power to override library exposed typeclasses behavior at any time by providing other instances in scope.
In the example above if you need keys serialized with Pascal Case style the user can just override the instance and leave the rest of the library intact:

Defined in user code (not in the json library), this implementation capitalizes the keys.

object extension Writes<Person> {
    fun writes(o: Person): JValue = JObject(listOf(
        "Name".toJson() to o.name.toJson(), // fails to compile if no `Reads<String>` is defined
        "Age".toJson() to o.age.toJson(), // fails to compile if no `Reads<Int>` is defined
    ))
}

Person("William Alvin Howard", 91).toJson() // { "Name": "William Alvin Howard", "Age": "91" }

The library author did not have to add specific support for Pascal Case because overriding a tiny bit of functionaly is easy enough. Entire system and libraries can be composed with typeclass evidences deferring dependencies to users but providing reasonable defaults that are verified at compile time therefore making programs more secure and making easier not to recurre to runtime reflection, costly initialization graph of dependencies etc.

I'll be happy to provide more use cases if needed but I believe the controlled and safe power type classes would bring to Kotlin is a great step forward in terms of compile time verification, type safety and performance for many common patterns like the ones demonstrated above.

There is also the use case of supporting typed FP with all the abstractions we used in examples previously but I will not bring those examples here because Type Classes is much more than FP, it's all about polymorphism and evidence based compile time verification opening the door to many other use cases most of them in trivial tasks users face in daily programming.

@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 5, 2017

@dnpetrov I would suggest that "type extension" like empty in Monoid are added to the scope of the corresponding type, so the examples below should just work:

fun <T> T.timesN(n: Int): T given T.Monoid {
    var result = T.empty() // because T is a Monoid.
    for (i in 0 until n) {
        result = result.combine(this) // again, because we are given that T is Monoid
    }
    return result
}
@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 5, 2017

@elizarov is :T given T.Monoid<T> needed or would it be :T given Monoid<T> ?

@elizarov

This comment has been minimized.

Copy link
Contributor

elizarov commented Oct 6, 2017

@raulraja That was a type. Fixed, than you. You can use either :T given Monoid<T> or :T given T.Monoid depending on the definition of Monoid -- either as extension interface Monoid<T> or extension interface T.Monoid.

The problem with the former, as was discussed before, is that it is not clear how to disambiguate the functions like empty without a Scala-like implicit parameters, which I particularly despise, because they wreck the symmetry between declaration and use of a function.

With the later declaration, given T.Monoid we can use T.empty() to disambiguate. The open questions, which I cannot answer without a larger design discussion and analysis of use-case if whether we should support unqualified empty() at all and whether a declaration of Monoid<T> (without a receiver type) with receiver-less empty() function should be even allowed.

@hannespernpeintner

This comment has been minimized.

Copy link

hannespernpeintner commented Oct 8, 2018

Trying to reconstruct what you talked about by myself in my brain - could work! Do you guys have a nice example for that?

@cypressious

This comment has been minimized.

Copy link
Contributor

cypressious commented Oct 8, 2018

Something along the lines of

extension interface Addition<T> {
    infix fun T.add(t: T): T
}

extension object IntAddition<Int> {
    inline fun Int.add(t: Int) = this + t
}

inline fun <T> sum(t1: T, t2: T, t3: T, with Addition<T>) = t1 add t2 add t3

fun main() {
    sum(1, 2, 3) //no boxing because of inlining
}
@SolomonSun2010

This comment has been minimized.

Copy link

SolomonSun2010 commented Oct 9, 2018

I don't suggest to invent new keywords, just reuse existence if no conflict ,Kotlin have too many keywords already.
For instance ,use :: or by field instead of with mentioned here:
Kotlin/kotlinx.coroutines#576 (comment)

extention keyword also is confused with extends : symbol. I think open init more better, meaning to open that class than add some operators and initialize a typeclass instance automatically for implicit parameters while resolution phrase.

open init class Ordering<T> {
    abstract fun compare(x: T, y: T): Int
}

In the future, if support more type level computing,probably use Haskell style data :: where etc.,
or abstract symbols as below :

typealias X // default as Nothing
typealias StringMap<V> as Map<String,V>
typealias Option<T> as {
					  if (T is Nothing) 
					  object None 
					  else 
					  object Some<T> 
}

typealias A|B as U where (U is A or U is B) 
typealias A&B as I where (I is A and I is B)

typealias List<C<_>> //as C where (C is Collection<_>)
typealias Tuple<A,B,C> as Tuple<A,Tuple<B,C>>

// Abstract type member
class Book<T>{
  typealias ID as T
  val id: ID
}

// readable pipe
operator infix fun <T, R> T.to(f: (T) -> R): R { f(this)}
operator infix fun <T, R> T.to(f: T.() -> R): R { f()}

5 to {+1} to {/ 2} to {3-}

@fvasco fvasco referenced this pull request Oct 9, 2018

Open

Inline classes #104

@raulraja

This comment has been minimized.

Copy link

raulraja commented Oct 9, 2018

@SolomonSun2010 where is used to denote sub-typing and I believe it will be confusing when applied to type class instances. As for abstract type members or path-dependent types, there is currently no intention on supporting those as part of this type classes and extension families proposal.

In your example all instances of where express a is a relationship but with type classes, all relationships are a has a since the composition is horizontal and not based on inheritance.

If you want to rephrase your example to target a Type class, Instance and the data type it provides the extensions for I think that then we can discuss if the syntax of extension and with needs to change. Feel free to submit your proposal as a PR to the current proposal. The current proposal is what is being used as spec in the POC regarding grammar changes and overall scope: arrow-kt/kotlin#6

@bkuchcik

This comment has been minimized.

Copy link

bkuchcik commented Nov 8, 2018

I read the readme file and that's a really great work. But one part of the syntax seems confusing to me:

inline fun <T> sum(t1: T, t2: T, t3: T, with Addition<T>) = t1 add t2 add t3

When you read the code, with Addition<T> wich is a type constraint is mixed with parameters. For a lot of developpers, it could be confusing.
Why the type constraint could not appear in the type description ?

inline fun <T with Addition<T>> sum(t1: T, t2: T, t3: T) = t1 add t2 add t3

Or even simply without a new keyword ?

inline fun <T:Addition<T>> sum(t1: T, t2: T, t3: T) = t1 add t2 add t3

Or to separate parameters and type constraints.

inline fun <T> sum(t1: T, t2: T, t3: T) (with Addition<T>) = t1 add t2 add t3`
@fvasco

This comment has been minimized.

Copy link

fvasco commented Nov 8, 2018

Hi @bkuchcik,
I endorsed this syntax.
Type class isn't a type bound constraint, but it is an implicit parameter. So I am inclined to avoid your first and second proposals due it does not figure in parameter list, so it is not really clear how to invoke the method from Java/ECMAScript.

In your third proposal I don't consider the parenthesis effective clearer than the with keyword.

@Bluexin Bluexin referenced this pull request Nov 9, 2018

Closed

proposal #1971

@raulraja

This comment has been minimized.

Copy link

raulraja commented Nov 9, 2018

@bkuchcik as @fvasco mentioned, it comes down to provide extra arguments that can be explicitly passed so someone not even used to the concept of type classes or injection of an instance can always instantiate and pass one as an argument.
Additionally, you can always provide a name for the type class argument and then it reads the same as when you have varargs:

inline fun <T> sum(t1: T, t2: T, t3: T, with intAddition: Addition<T>) = 
  t1 add t2 add t3 //ignore `intAddition` or use it here to pass it explicitly to the next dependent
@TAGC

This comment has been minimized.

Copy link

TAGC commented Nov 13, 2018

Is there any estimate on if/when this will get integrated into Kotlin? I think something like this would be really useful for me.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Nov 13, 2018

@TAGC A final proposal will be submitted for consideration next December or January. @truizlop is working on it.

truizlop and others added some commits Nov 14, 2018

Update proposal based on the initial implementation (#10)
* Update proposal based on the initial implementation

* Add suggested changes from comments
Fix misuse of encoding (#9)
* Fix misuse of encoding

* Fix typos
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 <a> duplicate(a : A, with M: Monoid<A>): A = a.combine(a)

This comment has been minimized.

@brendanzab

brendanzab Nov 15, 2018

Should <a> be <A>?

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.
@hannespernpeintner

This comment has been minimized.

Copy link

hannespernpeintner commented Nov 16, 2018

Have you considered removing the higher kinded types topic from this proposal and move it to its own? I don't think that hkt are too relevant for this proposal, but there are still some (unimplemented) examples in the documentation.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Nov 17, 2018

@hannespernpeintner yes, it needs to be updated by @truizlop once he is done with his implementation before submitting the final proposal by the end of this quarter or the beginning of the next one. In the meantime PRs addressing the current POC which does not introduce kinds are welcome.

@Wildsoft

This comment has been minimized.

Copy link

Wildsoft commented Dec 13, 2018

will it also support multi-parameter type classes?

@SolomonSun2010

This comment has been minimized.

Copy link

SolomonSun2010 commented Jan 4, 2019

Happ new year ! How about the progress ?

@raulraja

This comment has been minimized.

Copy link

raulraja commented Jan 4, 2019

@Wildsoft at the moment only type classes with one type parameter. Maybe @truizlop can elaborate if multi params are doable in the first draft.
@SolomonSun2010 The plan is to submit this proposal with a complete POC at the end of Q1 2019. @truizlop is working on that piece.

@fvasco

This comment has been minimized.

Copy link

fvasco commented Jan 4, 2019

Is fun hello(with appendable: Appendable) a valid function signature?

Is fun <A: Any?> add(a: A, b: A, with Monoid<A>?) a valid function signature?

The referenced document does not mention nothing about these.

@fvasco

This comment has been minimized.

Copy link

fvasco commented Jan 5, 2019

I played a bit with the code, should this KEEP allow implicits in Kotlin?

So using the interface

interface Implicit<T> {
    operator fun invoke(): T

    companion object {
        operator fun <T> invoke(t: T) = object : Implicit<T> {
            override fun invoke(): T = t
        }
    }
}

Can I write

fun main() {
    hello1(Implicit(System.out)) // declare implicit
}

fun hello1(with appendable: Implicit<Appendable>) {
    hello() // use implicit appendable
}

fun hello2(with appendable: Implicit<Appendable>) {
    appendable().append()
}

Finally I can declare a local variable implicit using an IIFE

fun main() {
    run {
        fun(with appendable: Implicit<Appendable>) { // declare implicit
            hello1() // use implicit
        }
    }(Implicit(System.out)) // assign implicit
}
@raulraja

This comment has been minimized.

Copy link

raulraja commented Jan 5, 2019

@fvasco Since what you call there Implicit<T> has the shape of a type class you could do that. People should not be scared of the word implicit as Kotlin extension functions are already a form of implicits. Type classes just allows you to expose them like that naturally because type class extensions are implicitly discovered by the compiler based on the resolution rules for extensions.

You can also do:

fun <A> inject(with ev: Implicit<A>): A = ev

And since Kotlin extensions as type classes are coherent and globally accessible use inject<Appendable> ad-hoc without carrying with constraints.

@fvasco

This comment has been minimized.

Copy link

fvasco commented Jan 5, 2019

Thank you, @raulraja,
the proposed KEEP misses of some details about interface type parameter arity?
Can I use for extension class interfaces like Interface (no generic) or Interface<A, B, C>? Is it an open question?

Can I define the function fun <A: Any?> add(a: A, b: A, with Monoid<A>), is the A type be nullable?
Finally can I define the function fun <A: Any> add(a: A, b: A, with Monoid<A>?), can the Monoid be optional?

These answers should be inserted in the document.

@pakoito

This comment has been minimized.

Copy link

pakoito commented Jan 9, 2019

An use case for KEEP-87 in kotlinx.coroutines: create a typeclass for exceptions to have their stacktraces enhanced with asynchronous jumps.

@nikita-leonov

This comment has been minimized.

Copy link

nikita-leonov commented Feb 6, 2019

@raulraja is there any way for others to contribute into the proposal? I am sure there are a lot of people waiting for this proposal to go out, and probably can help.

@raulraja

This comment has been minimized.

Copy link

raulraja commented Feb 7, 2019

@nikita-leonov the plan is that @truizlop is submitting the proposal at the end of Q1 2019. Contributions are welcome!, just PR the text of the proposal and if you want to get involved with implementation contact @truizlop helping wherever it's needed. The description in this PR includes links to the fork and instructions to get it up and running with the current proposal mostly implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment