Skip to content

Implement Java Interface with Frege Record #360

@matil019

Description

@matil019

This is a proposal which introduces a new syntax that allows Frege programmers to implement Java Interfaces with Frege Record declarations.

Proof of Concept

Supports the "Pure" case only: #361

Short description and Examples

native new functions take Frege records as their arguments and uses their fields to implement the methods of Java interface.

Pure

public interface J {
  public int succ(int i);
  public int succ(int i, int n);
}
data JImpl = JImpl
  { succ :: Int -> Int
  , succN :: Int -> Int -> Int
  }

data J = pure native J where
  pure native new "extends" :: JImpl -> J
  pure native succ :: J -> Int -> Int
  pure native succN succ :: J -> Int -> Int -> Int

main = do
    let j = J.new $ JImpl { succ = (+1) }
    println $ j.succ 1    -- 2
    let j = J.new $ JImpl { succ = (*10) }
    println $ j.succ 1    -- 10
    println $ j.succN 5 2 -- 500

Mutable

public interface Counter {
  public int get();
  public void increment();
}
data Impl = Impl
  { get :: ST s Int
  , increment :: ST s ()
  }

data Counter = native Counter where
  native new "extends" :: Impl -> STMutable s Counter
  native get :: Mutable s Counter -> ST s Int
  native increment :: Mutable s Counter -> ST s ()

main = do
    c <- do
        internal <- Ref.new 0
        Counter.new $ Impl
          { get = internal.get
          , increment = internal.modify succ
          }
    println =<< c.get    -- 0
    c.increment
    println =<< c.get    -- 1

Abstract class

public abstract class AC {
  public AC(int i) {
    // does something with i...
  }
  public abstract void doFoo();
}
data Impl = Impl { doFoo :: ST s () }

data AC = native AC where
  native new "extends" :: Impl -> Int -> AC
  native doFoo :: AC -> ST s ()

-- ...

Long description

Motivation

Java libraries and frameworks tend to provide Java interfaces, which client code must implement to work with them. Currently, in order to implement Java interfaces in Frege, Frege programmers must either (a) use the native module declarations, or (b) write Java source files separately. In either case, Frege programmers must work out the laziness of Frege arguments/return values and write boilerplates such as .call(), TST.performUnsafe(...), Thunk.lazy(...), etc. Since implementing interfaces and extending classes are the essential part of Java programming, having the compiler automate these tasks should greatly help writing Frege against existing huge Java codebase.

Concept

Implementing a Java interface is essentially providing a set of functions that implements its methods. "A set of (named) functions" can be represented with the record syntax in Frege, so currently one can write something like this:

module Foo where

native module where {
  public static I mk_(final TImpl impl) {
    return new I() {
      @Override
      public int getValue() {
        return TImpl.getValue(impl);
      }
    };
  }
}

data Impl = Impl { getValue :: Int }

data I = pure native I

pure native mk "Foo.mk_" :: Impl -> I

For concrete, working, real examples, please see modules such as this.

The native module part in the above example is a boilerplate and is to be automatically generated in the proposal. With the new syntax, it can be rewritten to:

data Impl = Impl { getValue :: Int }

data I = pure native I where
  pure native getValue :: I -> Int
  pure native new "extends" :: Impl -> I

Syntax

The new syntax introduced in this proposal is the Java-name "extends" in native function declarations:

[pure] native fregeName "extends"
  :: RecordType
  [-> constructor arguments (abstract classes only) ...]
  -> [STMutable s] NativeType (LHS of native data declaration)

Specifying "extends" instead of "new" generates an anonymous class declaration instead of a plain new expression.

Field/Method correspondence and Overload resolution

Since Java has method overloading, the syntax must support overload resolution. For this, it relies on the other native member declarations.

data Impl = Impl
  { add2 :: Int -> Int -> Int
  , add3 :: Int -> Int -> Int -> Int
  , ignored :: Int
  , garbage :: Int -> Bool
  }

data JavaI = pure native JavaI where
  pure native new "extends" :: Impl -> JavaI
  pure native add2 add :: JavaI -> Int -> Int -> Int
  pure native add3 add :: JavaI -> Int -> Int -> Int -> Int
  pure native garbage :: JavaI -> Int -> Int

Methods for an anonymous class are generated in the following algorithm:

  1. For each fields in a record type (Impl),
  2. Members of the target native type (JavaI) are looked up by Frege name
    (add2, add3, etc., but not add).
  3. If a matching member is found for the field, a method is generated with the Java name. For example, because Impl.add2 matches JavaI.add2, and JavaI.add2 has Java-name add,
    public int add(int arg1, int arg2) {
      return TImpl.add2(impl, Thunk.lazy(arg1), Thunk.lazy(arg2)).call();
      // where `impl` is the parameter of `JavaI.new`
    }
  4. Superfluous fields in the record (Impl.ignored) are ignored.
  5. If signatures are incorrect (Impl.garbage), it's a javac-time error.
  6. Both record fields and native members must exist for every methods of the interface. otherwise, it's a javac-time error.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions