"Extension types" for Kotlin


#21

Could you clarify, what does it mean “now” and “from this point on” in terms of compiler scopes? I.e. if I declare such an extension for String, where would I be able to use String as a Monoid?

Can I use List<String> where List<Monoid> is expected? If yes, how the conversion is performed?


#22

@ilya.gorbunov

I haven’t really thought about this in details but my first idea would be to use a similar mechanism as extension functions: you would need to import the Monoid type, obviously, and also the compilation unit that defines the override. In effect, the compiler could either emit a function that performs the conversion (and I understand this is something goes against Kotlin’s focus on avoiding implicit conversions) or it could emit a new synthetic class with the required attributes. The difficulty for the compiler is to remember that a String is also a Monoid wherever these conditions are met.


#23

To mitigate the implicit problem:

  • don’t allow chaining the conversions
  • force the extension types to be declared at the top level; much of the chagrin of Scala conversions it’s hard to know where they came from
  • (maybe?) in the same spirit, force the extension types to be imported explicitly (not included in wildcards) (*)
  • and (of course), mark extension types conversion clearly in the IDE

I’d still be useful and would place the system well into the “not surprising” territory.

I guess all this stuff really belong in a KEEP, no?

(*) Actually a much better solution would be to make the import system smart, and some syntax to toggle wildcard import of extension types on and off (off by default). It’s a bit unorthodox, but a good compromise (also see this).


#24

FYI, I posted a few additional thoughts on ad-hoc polymorphism here.


#25

I know :slight_smile:

Something I don’t like about your proposal though: that the extension functions implementing the extension type are not encapsulated somehow.

If you already have an extension function with the same name as a method in your interface (say zero for Monoid), things get awkward and you have to disambiguate in some fashion.

I would like it to be like this:

override class String: Monoid<String> {
    fun zero() : T { ... }
    fun append(a1: T, a2: T) : T { ... }
}
val x = "cthulhu".zero() // illegal
val y: Monoid<String> = "cthulhu"
val z = y.zero() // okay

#26

I believe the request to adapt something to something else is quite common, and that Kotlin could support it, but with an explicit conversion rather than implicit.

To make those explicit adapters less wordy, we could first implement KT-505 (delegating by signature), and second — provide some syntax to be able to write Monoid(stringValue) instead of object : Monoid by stringValue {}


#27

While I don’t have a concrete opinion on this subject, I thought in something less aggressive:

[structural] inline fun <reified T> doAdd(a: T, b: T) = a.add(b)

(Yes, I don’t like structural either, just an example.)

Since this method doAdd would be inlined by the compiler, it could be used with any T type which implements a add(other: T) method. The bytecode would be optimal (again, the function would be inlined, so I expect the add method call to be a natural invokevirtual operation.

Another example:

[structural] inline fun generateAccountId(value: Any) = "${value.name}@${value.id}"

Would work for any type which has id and name fields.


#28

This looks like poor man’s templates (as in C++ templates) restricted to functions only. Looking at C++ and JVM implementation it will create a lot of duplicate code (no good way to deduplicate it at linker level like is done in C++) without even encapsulating it in a function. The bigger problem is that it tends to create very hard to understand error messages when these are nested.


#29

Also like this .
:joy:


#30

I was looking at this and thinking about how to avoid implicits, but it seems like if that’s an important design parameter, typeclasses can/should just be done in the existing type system with an adapter-like pattern. I don’t like it, but having some way to do it is better than nothing. Sticking with @alex_delattre’s proposed syntax, here’s what it would look like:

interface Monoid<A> {
    fun zero(): A
    fun add(a1: A, a2: A): A
}

val stringMonoid = object : Monoid<String> {
    override fun zero() = ""
    override fun add(a1: String, a2: String) = a1 + a2
}

fun<A> multiply(a: A, n: Int, M: Monoid<A>) {
    var result: A = M.zero()
    for (i in 1 to n) {
        result = M.add(result, a)
    }
    return result
}

That seems like more-or-less the least common denominator of what JetBrains is willing to give us and the functionality people want (and it’s already possible).

One language feature that would make this pattern nicer (and maybe not break everything) is currying. Obviously we can mock currying with HOFs, but there’s non-negligible friction in doing things that way. With currying we could just do something like:

fun<A> multiply(M: Monoid<A>)(a: A, n: Int) {
    // ...
}

fun<A, B> funUsingTwoMonoids(aMonoid: Monoid<A>, bMonoid: Monoid<B>) {
    val aMult = multiply(aMonoid)
    val bMult = multiply(bMonoid)
    // do something, multiply some stuff
}

#31

KEEP for type classes https://github.com/Kotlin/KEEP/pull/87


#33

It sounds more like adding extension “static” functions for a “Class” rather than the “instances”. (Maybe is the extension of its companion object?)
So this case is different from that of “trait”.
But I don’t is more useful to inject a static function into another class. It cannot reuse more code. I prefer to write the static function under my own class name, after all doing so can make me less mix them up.