Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upType Classes for Kotlin #87
Conversation
This comment has been minimized.
This comment has been minimized.
|
The proposed syntax for companion constrains looks a bit confusing. Take a look at this example:
It is not perfectly clear from this declaration what is the receiver type of 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
Here, in the context of
In this approach 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:
In this approach
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:
can be replaced, for the same effect, with
or, which is not possible with companion objects, if the access to private members of
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 |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 2, 2017
|
@elizarov makes perfect sense. I'll update the proposal to the new proposed syntax |
mkobit
reviewed
Oct 2, 2017
| } | ||
| ``` | ||
|
|
||
| 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.
This comment has been minimized.
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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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 etc |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
How type class implementations ( Elaborating on the example with
and, for example,
and
Now, in package |
This comment has been minimized.
This comment has been minimized.
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.
Another approach would be to place further constrains in the instances like is done today with the
|
This comment has been minimized.
This comment has been minimized.
|
Let's assume the example by @dnpetrov
It is pretty straightforward with star imports:
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:
This will bring in scope all 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. |
This comment has been minimized.
This comment has been minimized.
|
The interesting side-effect of this proposal is that we can almost deprecate
Now a function that was doing something like:
can be replaced with:
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. |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 3, 2017
|
@elizarov that will simplify most of our code a ton since we are currently relying on |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 4, 2017
|
Updated the proposal with a section on overcoming some of the limitations of |
This comment has been minimized.
This comment has been minimized.
|
What if I have multiple type class implementations for the same type in the same package?
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:
Then |
This comment has been minimized.
This comment has been minimized.
|
With parametric type classes, type class dependent functions might need some different syntax, like:
|
This comment has been minimized.
This comment has been minimized.
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 @dnpetrov In your example above an alternative way to express 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:
That also is an alternative way to handle ambiguity when collisions exists beside using concrete imports. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
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:
then write
then declare functions with an explicit type class instance:
It is even shorter than the latest syntactic proposal for typeclasses, since I don't have to add 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. |
This comment has been minimized.
This comment has been minimized.
|
A nitpick but in the examples, I think you should be more specific about the name of the |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 4, 2017
•
|
@elizarov Kategory uses interfaces for typeclasses now and does it like that. The issue is that:
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 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:
And in a polymorphic context:
|
This comment has been minimized.
This comment has been minimized.
|
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 Moreover, instead of a having to reserve a new
This syntax also scales to multiple types (and, maybe HKTs, but I will not cover them here). For example, you can have:
However, as soon as you allow applying this syntax to multiple types, the story with "standalone" (static? but not really) functions like 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 (
In this context, Note, that we can allow to add additional types in Similarly, declaration of typeclass instances can ready follow the model of Kotlin's
Since we have indicated that Alas, this notation makes it less obvious on how to specify typeclass constraints for functions. The interface-inspired syntax of
|
This comment has been minimized.
This comment has been minimized.
This approach has some issues with overloading. E.g., what if I have several different type classes (extension interfaces, whatever) for I'd expect something like:
|
This comment has been minimized.
This comment has been minimized.
|
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). @raulraja @elizarov |
This comment has been minimized.
This comment has been minimized.
d3xter
commented
Oct 5, 2017
|
One possible motivation could be ad-hoc polymorphism as described by Cedric. For example in klaxon to create a new Json-Object you have the following method: This basically allows everything to be put inside. With typeclasses we can define a JSONValue:
Then all you need inside your JSON-Library is a method like that:
|
This comment has been minimized.
This comment has been minimized.
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
In the example above we used Type restrictions for safer operations
Type safe composable encoder / decoders and reflectionless instrospectionExpanding on what @d3xter mentions in #87 (comment)
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. Defined in user code (not in the json library), this implementation capitalizes the keys.
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. |
This comment has been minimized.
This comment has been minimized.
|
@dnpetrov I would suggest that "type extension" like
|
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 5, 2017
|
@elizarov is |
This comment has been minimized.
This comment has been minimized.
|
@raulraja That was a type. Fixed, than you. You can use either The problem with the former, as was discussed before, is that it is not clear how to disambiguate the functions like With the later declaration, given |
This comment has been minimized.
This comment has been minimized.
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? |
This comment has been minimized.
This comment has been minimized.
|
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
} |
This comment has been minimized.
This comment has been minimized.
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.
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., 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-} |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Oct 9, 2018
|
@SolomonSun2010 In your example all instances of If you want to rephrase your example to target a |
This comment has been minimized.
This comment has been minimized.
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 t3When you read the code, inline fun <T with Addition<T>> sum(t1: T, t2: T, t3: T) = t1 add t2 add t3Or even simply without a new keyword ? inline fun <T:Addition<T>> sum(t1: T, t2: T, t3: T) = t1 add t2 add t3Or to separate parameters and type constraints. inline fun <T> sum(t1: T, t2: T, t3: T) (with Addition<T>) = t1 add t2 add t3` |
This comment has been minimized.
This comment has been minimized.
fvasco
commented
Nov 8, 2018
|
Hi @bkuchcik, In your third proposal I don't consider the parenthesis effective clearer than the |
This comment has been minimized.
This comment has been minimized.
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. 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 |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Nov 13, 2018
•
truizlop
and others
added some commits
Nov 14, 2018
brendanzab
reviewed
Nov 15, 2018
| 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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
Wildsoft
commented
Dec 13, 2018
|
will it also support multi-parameter type classes? |
This comment has been minimized.
This comment has been minimized.
SolomonSun2010
commented
Jan 4, 2019
|
Happ new year ! How about the progress ? |
fvasco
referenced this pull request
Jan 4, 2019
Closed
Suggestion: Scoped(multi-scope) Coroutines #927
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
fvasco
commented
Jan 4, 2019
•
|
Is Is The referenced document does not mention nothing about these. |
This comment has been minimized.
This comment has been minimized.
fvasco
commented
Jan 5, 2019
|
I played a bit with the code, should this KEEP allow implicits in Kotlin? So using the interface
Can I write
Finally I can declare a local variable implicit using an IIFE
|
This comment has been minimized.
This comment has been minimized.
raulraja
commented
Jan 5, 2019
•
|
@fvasco Since what you call there You can also do: fun <A> inject(with ev: Implicit<A>): A = evAnd since Kotlin extensions as type classes are coherent and globally accessible use |
This comment has been minimized.
This comment has been minimized.
fvasco
commented
Jan 5, 2019
|
Thank you, @raulraja, Can I define the function These answers should be inserted in the document. |
pakoito
referenced this pull request
Jan 9, 2019
Open
Version 1.1.0 breaks referential equality of thrown exceptions #921
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. |
raulraja commentedOct 2, 2017
•
edited
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