eclipse-archived / golo-lang

Golo - a lightweight dynamic language for the JVM.
http://golo-lang.org/
Eclipse Public License 2.0
478 stars 91 forks source link

Issues when using closures or adapters for functional interfaces or SAM having default methods or method defined in Object #491

Open utybo opened 7 years ago

utybo commented 7 years ago

The Java interface java.util.Comparator has, for some reason, the equals method as an abstract method. Even though it is implemented by java.lang.Object for every object, this seems to confuse Golo which spits out a There is no implementation or override for: public abstract boolean java.util.Comparator.equals(java.lang.Object) when trying to create an adapter. This also makes Golo confused when only using a closure instead of an adapter (i.e using a closure as a functional interface) where a Comparator is expected.

Error stack :

Exception in thread "AWT-EventQueue-0" org.eclipse.golo.runtime.adapters.AdapterDefinitionProblem: There is no implementation or override for: public abstract boolean java.util.Comparator.equals(java.lang.Object)
    at org.eclipse.golo.runtime.adapters.AdapterDefinition.checkMethodsToBeImplemented(AdapterDefinition.java:181)
    at org.eclipse.golo.runtime.adapters.AdapterDefinition.validate(AdapterDefinition.java:91)
    at gololang.AdapterFabric.maker(AdapterFabric.java:158)
    at java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)
    at org.eclipse.golo.runtime.MethodInvocationSupport.fallback(MethodInvocationSupport.java:282)
    at gololang.Adapters$gololang$Adapters$types$adapter.maker(adapters.golo:106)
    at java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)
    at org.eclipse.golo.runtime.MethodInvocationSupport.fallback(MethodInvocationSupport.java:282)
    at gololang.Adapters$gololang$Adapters$types$adapter.newInstance(adapters.golo:129)
    at java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)
    at org.eclipse.golo.runtime.MethodInvocationSupport.fallback(MethodInvocationSupport.java:282)

Code :

Adapter(): interfaces(["java.util.Comparator"])
    : implements("compare", |this,e1,e2| {
      # Implementation here
    }): newInstance()
yloiseau commented 7 years ago
require(
  1 == java.util.Collections.min(
    list[5, 1, 2, 3],
    asInterfaceInstance(
      java.util.Comparator.class,
      |a, b| -> Integer.compare(a, b))),
  "err")

works as expected...

utybo commented 7 years ago

Huh, I forgot about that method... I expected it to work by just putting a closure but it seems Comparator doesn't count as a functional interface. Adapters still yield an error though I wonder if it's relevant since there is this asInterfaceInstance function

Sorry for my ignorance >_<

yloiseau commented 7 years ago

It should be automatically converted, but we have a bug in identifying SAM since 1.8, since now interfaces can have default methods. The asInterfaceInstance is a way to force the conversion explicitly.

Although in this particular case, the adapter is not necessary, I think the found behavior should be investigated, since it can be the sign of an underlying bug.

yloiseau commented 7 years ago

Moreover, Comparator is a functional interface, so it should work. Indeed,

require(
  1 == java.util.Collections.min(
    list[5, 1, 2, 3],
    asFunctionalInterface(
      java.util.Comparator.class,
      |a, b| -> Integer.compare(a, b))),
  "err")

fails (since it only creates an adapter under the hood), as well as

require(
  1 == java.util.Collections.min(
    list[5, 1, 2, 3],
    |a, b| -> Integer.compare(a, b)),
  "err")

failing to wrap the closure in a FI using a LambdaMetafactory

yloiseau commented 7 years ago

I actually have 2 old branches try to fix this aspect (https://github.com/yloiseau/golo-lang/tree/improvement/java-function-integration and https://github.com/yloiseau/golo-lang/tree/wip/java-function-integration) if someone wants to have a look :smile:

yloiseau commented 7 years ago

Here are some minimal tests:

@FunctionalInterface
public interface FI {
  int get();
}
@FunctionalInterface
public interface FiWithDefault {
  int get();

  default int apply() {
    return get() + 1;
  }
}

@FunctionalInterface
public interface FiWithEquals {
  int get();
  boolean equals(Object other);
}

public interface SAM {
  int get();
}
public interface SamWithDefault {
  int get();

  default int apply() {
    return get() + 1;
  }
}
public interface SamWithEquals {
  int get();
  boolean equals(Object other);
}

public final class Utils {

  public static int useSam(SAM f) {
    return f.get();
  }

  public static int useFi(FI f) {
    return f.get();
  }

  public static int useSamWithDefault(SamWithDefault f) {
    return f.get();
  }

  public static int useFiWithDefault(FiWithDefault f) {
    return f.get();
  }

  public static int useSamWithDefaultUseDefault(SamWithDefault f) {
    return f.apply();
  }

  public static int useFiWithDefaultUseDefault(FiWithDefault f) {
    return f.apply();
  }

  public static int useSamWithEquals(SamWithEquals f) {
    return f.get();
  }

  public static int useFiWithEquals(FiWithEquals f) {
    return f.get();
  }
}

test.golo:

module Test

import gololang.AnsiCodes
import gololang.Adapters

function title = |t| {
  println("")
  bold()
  fg_magenta()
  println(t)
  reset()
}

function test = |d, e, f| {
  print("▶ ")
  fg_blue()
  print(d)
  reset()
  print(": [")
  try {
    let r = f()
    require(r == e, "expected %s, got %s": format(e, r))
    fg_green()
    print("OK")
    reset()
    println("]")
  } catch (e) {
    if (e oftype AssertionError.class) {
      fg_yellow()
      print("FAILED")
      reset()
      println("]")
      fg_cyan()
      println("  " + e: message())
      reset()
    } else {
      fg_red()
      print("ERROR")
      reset()
      println("]")
      fg_cyan()
      println("  " + e)
      reset()
    }
  }
}

function main = |args| {
  title("Pure SAM and FI")
  test("sam with closure", 42,
    -> Utils.useSam(-> 42))
  test("fi with closure", 42,
    -> Utils.useFi(-> 42))
  test("sam asInterfaceInstance", 42,
    -> Utils.useSam(asInterfaceInstance(SAM.class, -> 42)))
  test("fi asFunctionalInterface", 42,
    -> Utils.useFi(asFunctionalInterface(FI.class, -> 42)))

  title("SAM and FI with default method (use abstract)")
  # fails since SamWithDefault is not identified as a SAM due to the default
  # method (see o.e.g.runtime.TypeMatching.isSam)
  test("sam with closure", 42,
    -> Utils.useSamWithDefault(-> 42))
  test("fi with closure", 42,
    -> Utils.useFiWithDefault(-> 42))
  test("sam asInterfaceInstance", 42,
    -> Utils.useSamWithDefault(asInterfaceInstance(SamWithDefault.class, -> 42)))
  test("fi asFunctionalInterface", 42,
    -> Utils.useFiWithDefault(asFunctionalInterface(FiWithDefault.class, -> 42)))

  title("SAM and FI with default method (use default)")
  # fails since SamWithDefault is not identified as a SAM due to the default
  # method (see o.e.g.runtime.TypeMatching.isSam)
  test("sam with closure", 42,
    -> Utils.useSamWithDefaultUseDefault(-> 41))
  test("fi with closure", 42,
    -> Utils.useFiWithDefaultUseDefault(-> 41))

  # fails: the MethodHandleProxies does not define nor use the default interface method.
  test("sam asInterfaceInstance", 42,
    -> Utils.useSamWithDefaultUseDefault(asInterfaceInstance(SamWithDefault.class, -> 41)))

  # fails: the adapter does not define nor use the default interface method.
  test("fi asFunctionalInterface", 42,
    -> Utils.useFiWithDefaultUseDefault(asFunctionalInterface(FiWithDefault.class, -> 41)))
  test("sam with adapter (manual delegate)", 42,
    -> Utils.useSamWithDefault(Adapter(): interfaces(["SamWithDefault"])
        : implements("get", |this| -> 42)
        : implements("apply", |this| -> ^SamWithDefault::apply(this))
      : newInstance()))
  test("fi with adapter (manual delegate)", 42,
    -> Utils.useFiWithDefault(Adapter(): interfaces(["FiWithDefault"])
        : implements("get", |this| -> 42)
        : implements("apply", |this| -> ^FiWithDefault::apply(this))
      : newInstance()))

  title("SAM and FI with equals")
  # fails since SamWithEquals is not identified as a SAM due to the abstract
  # equals method, even if inherited from Object (see o.e.g.runtime.TypeMatching.isSam)
  test("sam with closure", 42,
    -> Utils.useSamWithEquals(-> 42))
  test("fi with closure", 42,
    -> Utils.useFiWithEquals(-> 42))
  test("sam asInterfaceInstance", 42,
    -> Utils.useSamWithEquals(asInterfaceInstance(SamWithEquals.class, -> 42)))

  # fails: the adapter does not define nor use the inherited equals method.
  test("fi asFunctionalInterface", 42,
    -> Utils.useFiWithEquals(asFunctionalInterface(FiWithEquals.class, -> 42)))

  # fails: the adapter does not accept the equals overriding. Why ?
  # probably a conflict with the one inherited from Object
  test("sam with adapter (override)", 42,
    -> Utils.useSamWithEquals(Adapter(): interfaces(["SamWithEquals"])
        : implements("get", |this| -> 42)
        : overrides("equals", |super, this, other| -> this == other)
      : newInstance()))

  # fails: the adapter does not accept the equals implementation. Why ?
  # probably a conflict with the one inherited from Object
  test("sam with adapter (implements)", 42,
    -> Utils.useSamWithEquals(Adapter(): interfaces(["SamWithEquals"])
        : implements("get", |this| -> 42)
        : implements("equals", |this, other| -> this == other)
      : newInstance()))
}

Makefile:

CLASSES := $(addsuffix .class, $(basename $(wildcard *.java)))

.PHONY: test
test: $(CLASSES)
    @golo golo --classpath . --files test.golo

%.class: %.java
    @javac -d . $<

clean: 
    @rm -f *.class