Skip to content
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

[WIP] Prototype implementation of KEEP-87 #6

Open
wants to merge 65 commits into
base: master
from

Conversation

Projects
None yet
5 participants
@truizlop
Copy link
Member

commented Sep 25, 2018

Background

The goal of this PR is to show a prototype implementation of the KEEP-87 proposal to add Typeclasses to Kotlin. Further details about this proposal can be read in this link.

For the rest of the document, we can assume the existence of the following typeclass:

interface Semigroup<A> {
  fun A.combine(b: A): A
}

Changes

This prototype introduces the following changes to the Kotlin language:

Syntax

  • with keyword

Parameters to functions (including methods and constructors) can be prepended with the with keyword to indicate the parameter is an implicit value. The following code is now valid in Kotlin:

fun <A> plus(a: A, b: A, with semigroup: Semigroup<A>) { ... }
class Merger<A>(with val semigroup: Semigroup<A>) { ... }
class Duplicator<A> {
  fun duplicate(a: A, with semigroup: Semigroup<A>): A { ... }
}

It is important to add val to implicit class fields to make sure they are accessible in the scope of the class.

  • extension keyword

Classes and Interfaces can be modified with the extension keyword to indicate they provide an implementation for a given typeclass. Also, singleton objects can be modified with extension. The following code is now valid in Kotlin:

extension object IntSemigroup : Semigroup<Int> { ... }
extension class OptionSemigroup<A>(with val semigroup : Semigroup<A>) : Semigroup<Option<A>> { ... }

Semantics

  • Resolution of implicit parameters in function arguments

The with keyword adds the value to the scope of the function. The following functions are equivalent:

// Kotlin + KEEP-87
fun <A> plus(a: A, b: A, with semigroup: Semigroup<A>): A = a.combine(b)

// Regular Kotlin
fun <A> plus(a: A, b: A, semigroup: Semigroup<A>): A = with(semigroup) { a.combine(b) }
  • Resolution of implicit parameters in class fields

The with keyword adds the value to the scope of every method in the class. The following classes are equivalent:

data class Wrapper<A>(val value: A)
// Kotlin + KEEP-87
extension class WrapperSemigroup<A>(with val semigroup: Semigroup<A>) : Semigroup<Wrapper<A>> {
  override fun Wrapper<A>.combine(b: Wrapper<A>): Wrapper<A> = 
      Wrapper(this.value.combine(b.value))
}

// Regular Kotlin 
class WrapperSemigroup<A>(val semigroup: Semigroup<A>) : Semigroup<Wrapper<A>> {
  override fun Wrapper<A>.combine(b: Wrapper<A>): Wrapper<A> = 
      with(semigroup) { Wrapper(this@combine.value.combine(b.value)) }
}
  • Invocations to functions with implicit parameters

Functions with implicit parameters can be passed all values, or implicit ones can be omitted and let the compiler find the suitable class to fill a value. Assuming class IntSemigroup is modified with extension and in the right location (see instantiation section below), the following invocations are equivalent:

val x = plus(1, 2, IntSemigroup()) // x == 3
val y = plus(1, 2) // y == 3
  • Instantiation of implicit parameters

The current implementation attempts to find a suitable implementation of a given typeclass in this order:

  1. Check for a valid instance in the scope of the invocation. In the following code:
fun <A> duplicate(a: A, with semigroup: Semigroup<A>): A = plus(a, a)

The third argument of plus is resolved to the semigroup argument of the duplicate function. Note that if the function duplicate doesn't include the implicit semigroup argument, it wouldn't be possible to resolve the necessary instance for plus, as there is not enough information to get a concrete instance.

  1. Check for an extension class in the package where the function is being called.
  2. Check for an extension class in the companion object of the type argument of the typeclass.
  3. Check for an extension class in the companion object of the typeclass.
  4. Check for an extension class in the subpackages of the type argument of the typeclass (must be internal).
  5. Check for an extension class in the subpackages of the typeclass (must be internal).

As an example, consider the following code:

interface Semigroup<A> {
    fun A.combine(b : A) : A

    companion object {
        extension class IntSemigroup : Semigroup<Int> {
            override fun Int.combine(b: Int): Int = this + b
        }
    }
}

fun <A> duplicate(a: A, with semigroup: Semigroup<A>): A = a.combine(a)

fun foo() {
  print(duplicate(2))
}

The function call duplicate(2) needs an instance of Semigroup<Int>; the resolution process goes as follows:

  1. Look for parameters that may implement Semigroup<Int> in the scope of the invocation. foo does not have any parameters, so there is no candidate.
  2. Look for extension classes in the package where it is being called. There are no implementations in the package for the requested type.
  3. Look for extension classes in the companion object of Int. There are no implementations.
  4. Look for extension classes in the companion object of Semigroup. There is a valid class IntSemigroup. It is instantiated and passed as a parameter.

In case all this 4 checks fail to resolve a unique instance, there is a compilation error. If, in the process of instantiating an extension class, there is a constructor that needs additional implicit parameters, the process is performed recursively.

Compiler reported errors

Inline errors while coding (using inspections and red underline)

Screenshot 2019-04-05 at 12 51 11

Errors once you hit the "compile" button:

Screenshot 2019-04-05 at 12 59 54

How to try - approach 1

  • Clone this project and checkout the keep-87 branch.
  • Follow the instructions on the README configure the necessary JVMs.
  • Follow the instructions on the README to open the project in IntelliJ IDEA.
  • Once you have everything working, you can run a new instance of IntelliJ IDEA with the new modifications to the language by executing ./gradlew runIde. There is also a pre-configured run configuration titled IDEA that does this.
  • It will open a new instance of the IDE where you can create a new project and experiment with the new features of the language. You can also download this project
    ) with some sample code that you can try out.

How to try - Alternative approach (easier)

  • Download the latest version of IntelliJ IDEA 2018.2.4 from JetBrains
  • Go to preferences -> plugins section.
  • Click on "Manage Plugin Repositories".
    InstallKeepFromRepository1
  • Add our Amazon s3 plugin repository as in the image.
    InstallKeepFromRepository2
  • Now browse for "keep87" plugin.
    InstallKeepFromRepository3
  • Install it.
    InstallKeepFromRepository4
  • Download and run this project on that IntellIJ instance.
@@ -72,6 +72,10 @@ public class ClassReference(override val jClass: Class<*>) : KClass<Any>, ClassB
override val isInner: Boolean
get() = error()

@SinceKotlin("1.1")

This comment has been minimized.

Copy link
@pakoito

pakoito Sep 25, 2018

Member

1.4? 1.5? :D

This comment has been minimized.

Copy link
@truizlop

truizlop Sep 25, 2018

Author Member

This is temporary, there are other places where the version number needs to be changed, but stating higher versions made it impossible to launch the IDE to test the new behavior.

if (candidate.constructor.toString().equals(target.constructor.toString())) {
return true
}
if ((findSubstitution(candidate, substitutions)?.constructor?.toString() ?: "").equals(target.constructor.toString())) {

This comment has been minimized.

Copy link
@pakoito

pakoito Sep 25, 2018

Member

Is there any reason to use equals here instead of ==?

This comment has been minimized.

Copy link
@truizlop

truizlop Sep 26, 2018

Author Member

I think I was comparing the constructor with equals, then switched to toString

@pakoito

This comment has been minimized.

Copy link
Member

commented Sep 25, 2018

Great job :D I'm just checking stuff for shits and giggles, it's all awesome!

@JorgeCastilloPrz

This comment has been minimized.

Copy link
Member

commented Sep 30, 2018

Should we also experiment on adding support for extension val? ideally vals are also potential nested providers, since they are also functions (get()).

@hannespernpeintner

This comment has been minimized.

Copy link

commented Oct 5, 2018

You, sirs, are awesome men! Many kudos for the prototype implementation.

I gave the current state a try, super nice already :) @truizlop I think it's clear that there are some missing pieces, most of them are probably already on your radar. Do you wish to get feedback for the current state? If yes, how do you like it - as issues here on github? I just want to prevent to shower you with issues that are most likely solved as one of the next implementation steps.

Thank you again, really great work!

@truizlop

This comment has been minimized.

Copy link
Member Author

commented Oct 5, 2018

@hannespernpeintner Thanks for your very kind words! I'd love to hear about your feedback and I think the best way is to open issues in this repository so that everyone can keep track of what we are doing and openly discuss the best way forward.

@hannespernpeintner

This comment has been minimized.

Copy link

commented Oct 30, 2018

What is the current state of the prototype? Is there anything where one could support you? :)

@truizlop

This comment has been minimized.

Copy link
Member Author

commented Oct 30, 2018

Hi Hannes, we are still adding support to different use cases. Right now, the best way you can help us is trying the current status of the plugin and let us know which cases are not working properly. For instance, we have noticed that inlining and implicit instances are not working properly. All these corner cases are going to help us drive our efforts and stabilize the proposal before submitting it. So, the more you try it and find use cases where it is ambiguous or does not behave as expected, the better it is for us.

@hannespernpeintner

This comment has been minimized.

Copy link

commented Nov 5, 2018

I invested some more time to create test cases with nested typeclass usage, with inline classes and I wasn't able to produce any errors besides the ones we already had a s a ticket. Would be interesting if you could share which inline class use case didn't work out for you.

Also, how's the current state of the extension object/extension val implementation? There aren't any new commits, I think :)

@hannespernpeintner

This comment has been minimized.

Copy link

commented Nov 6, 2018

Hi there i have another question. I tried to implement the reified scenario that came Up in the proposal thread. This is based on type reification through inline functions. Inline functions can't be used through Interfaces or virtual functions in general. However, with an abstract class as type class, this would be possible. Have we talked about abstract classes somewhere already? Is there a reason why abstract classes should be prohibited? The current Implementation doesn't allow them afaik.

EDIT: Please ignore what I said. I had strange issues with my IDE, the use of an abstract class actually works, which is super nice :) I'm going to post the example when I got it right :)

JorgeCastilloPrz and others added some commits Apr 5, 2019

Merge pull request #25 from arrow-kt/jc-remove-implicit-naming-and-an…
…y-fp-related-wording-from-keep

* rename ImplicitValueArgument

* rename generateImplicit() function

* rename findImplicitParameters function from AsmUtil

* rename isImplicit flag

* rename ImplicitCandidate

* rename ClosureCodegen implicit mentions

* rename JvmCodegenUtil occurrences

* rename CompatibilityResult

* rename extension candidate resolution

* rename ImplicitResolution

* replace implicit in error messages by extension

* rename any implicit or typeclass occurrences in ExtensionResolution

* rename extension resolution strategy

* rename other occurrences in ExtensionResolutionStrategy

* rename occurrences in BodyResolver

* rename language feature

* rename implicit argument

* rename typeclass tests

* regenerate test sources

* Update test for complex resolution

* Update test for resolution from function parameter

* Update test for explicit resolution

* Update test for concrete type resolution

* Update test for resolution in companion object of extension contract

* Fix bug returning duplicate candidates

* Update test for resolution in subpackage of the extension contract

* Update test for resolution in the same package

* Update test for resolution of nested instances

* Update test for resolution in type companion

* Update test for resolution in type subpackages

* Update error reporting tests

* Update parsing tests

* Update resolution tests

@JorgeCastilloPrz JorgeCastilloPrz changed the title [WIP] Prototype implementation of KEEP-87 proposal to add Typeclasses to Kotlin [WIP] Prototype implementation of KEEP-87 Apr 10, 2019

@@ -694,7 +694,7 @@ val deployKeepMetadata by task<AmazonS3FileUploadTask> {

val localMetadata = file("$rootDir/idea/src/META-INF/plugin.xml")
val newKeepVersion = localMetadata.readText().let { content ->
content.substring(content.indexOf("<version>") + 1, content.indexOf("</version>"))
content.substring(content.indexOf("<version>") + 9, content.indexOf("</version>"))

This comment has been minimized.

Copy link
@JorgeCastilloPrz

JorgeCastilloPrz Apr 10, 2019

Member

This bug was causing the deployed version to not be properly generated.

@sellmair

This comment has been minimized.

Copy link

commented May 16, 2019

Hey guys! Awesome work so far. I wanted to start playing around with the proposal now, but I have trouble getting the IntelliJ plugin installed. So far I tried versions 2019.1.2 and 2018.2.8. Both versions will prompt me to restart after the keep87 plugin was installed. After the IDE restart, the plugin seems to be not installed though. I retried several times.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.