"Extension types" for Kotlin


#7

You’re probably right, it is more about extending rather than modifying.

How about some sugar (borrowing from Swift) like this?

extend class String : IMonoid {
    fun zero() = ""
    fun append(other: String) = this + other
}

#8

If you go this way, then you will have to repeat the entire definition of IMonoid for each class you want to add to this type class.

I think IMonoid should be defined once and then its name can be reused whenever a new class wants to be part of that family.

I’m not married to my suggestion of using override for this, it was just the first idea I had with the constraint of not adding a new keyword, I’m sure we can come up with other syntaxes.


#9

I saw the extend class as just some added sugar to remove the need for repeating the Type.function() syntax, rather than explicitly implementing the interface. I envisioned it being the equivalent to your initial example.


#10

In the past, JetBrains has been strongly against the idea of implicit conversions (which this essentially amounts to - importing the extension would implicitly extend the type of the object).

Ages ago a conversion operator was proposed allowing types to be converted. I kind of liked the idea, but think its been dropped along the way. See: https://youtrack.jetbrains.com/issue/KT-1752#comment=27-318887

I guess your proposal avoids the “boxing” overhead that conversions would introduce however.


#11

the “Nice” programming language had an Idea for how to handle this and still work with the Java Type system, Generics.

http://nice.sourceforge.net/manual.html#abstractInterfaces

Abstract Interface are special Interfaces which can only be used as to close a generic argument (like < A : AbstractInterface > ) , you cannot have handle to the type, except in a generic context, it appears to comes down to a compiler trick, like a macro, maybe similar Kotlin’s reified generics, or Go’s interfaces


#12

Nice! (pun semi-intended)
Seems quite similar to what I proposed here: Kotlin and the Expression Problem


#13

But you’re going to have to do that anyway, even with your original suggestion, correct? Or do you mean classes that have a subset of the interface already implemented?


#14

Right, not sure what I was thinking when I wrote that…


#15

Hello,

Here is another (draft) approach to typeclass inspired by scala implicit resolution (but limited to typeclasses use case)

`

//Typeclass interface definition
interface Monoid<T> {
    fun zero(): T
    fun add(a1: T, a2: T): T
}

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

//Int instance
val intMonoid = object : Monoid<Int> {
    override fun zero() = 0
    override fun add(a1: Int, a2: Int) = a1 + a2
}

//A generic function working on all type T, where it exists a Monoid<T>
//Such instance is callable in the function body as M
fun <T with Monoid as M> multiply(a: T, n: Int) {
    var t: T = M.zero()
    for (i in 1 to n) {
        M.add(a)
    }
    return t
}

fun main(args: Array<String>) {
    println(multiply("hello", 5)) // should print "hellohellohellohellohello"
    println(multiply(4, 5)) // should print 20
    println(multiply(45.3, 5)) // should not compile because no Monoid<Double> is defined
}

`


#16

I think the important part of such suggestions is the resolution of the implicit, which is the Monoid interface in this case. What are your thoughts on that?

  • Is it an import at the call/usage site?
  • How can you be explicit about the type class being used, e.g. can I have intMonoid1 and intMonoid2?
  • What about hierarchies, e.g. having an anyMonoid and a stringMonoid?

To me such resolution of implicit classes should in fact be very very explicit. No compiler burden, but simple straightforward rules that are easy to understand and (relatively) easy to implement.

There are also some other important questions, questions like:

  • What will the java signature be?
  • Or doesn’t it exist on the java site, will it always be inlined?

#17

What may be a solution would be to allow the following to work (and be statically resolved)

inline fun <reified T> add(v1:T, v2:T) = when (T) {
  Int -> v1 + v2
  BigDecimal -> v1.add(v2)
} 

This code should not require an else clause, but a compile error instead. The non-applicable cases should also be elided/omitted from the compilation result as they are statically unreachable. Usage of T.class with the same eliding would be ok as well.


#18

I’d like to throw my $0.02 in also - personally I love the idea of “Extension Types” as well. I do a lot of work with JavaMail, which pre-dates the AutoCloseable/try-with-resources functionality. Even though some of the classes in there have explicit close methods, I still have to add a finally block to manually close them. It’d be awesome to just do something like (using @CodeAnxiety’s sample syntax):

extend class Store : AutoCloseable {
    fun close() {
        (this as Store).close() // well, some kind of way to refer to the base method instead of trying to recurse.
    }
}

createEmailStore().use {
    // blah blah blah 
}

On one hand, I can see this being consider an implicit conversion - it basically is. However, I don’t think it’s terribly different from extension methods. Those are kinda-sorta implicit conversions - you’re converting from a type that doesn’t have the functionality you want into one that does. These would still be a decorator for the original object, just one that implements a specified interface instead of one that just adds a couple of methods.

The only possible gotcha I can see off hand would be if the extended class already possesses (some) methods with the same signature; how would those be resolved? I imagine whatever resolution method is in place for extension methods would work as well, but maybe not.

One other possibility I remember from my C# days was that you had the ability to explicitly define an implementation for an interface in the case of naming conflicts - you could have AutoCloseable.close() and FrontDoor.close(). Depending on the context the object was used in, it would call which ever one was appropriate; if the object was referenced as a FrontDoor instance, it would call the FrontDoor.close() implementation. If you omitted the type qualifier and only had a single implementation, it would just call that regardless of the context.


#19

I really like @cedricbeust 's proposal (apart from the syntax). It would give us simple type classes for almost no additional complexity or performance cost. In Kotlin they could appear as regular interfaces. The only problem would be that you couldn’t use those interfaces in Java. But at least you don’t have to learn a new concept on the Java side to interact with those methods. I think this is a fair trade off.

What does the Kotlin team think of it? I couldn’t find any responses in this thread.


#20

The idea is indeed very similar to implicit conversions, for which we’ve made an explicit decision not to include them in the language.


#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 {}