Skip to content

Calling Frege from Java (from release 3.24 on)

Ingo Wechsung edited this page Mar 11, 2016 · 14 revisions

This guide explains how to call code generated by the Frege compiler from Java. It is valid for releases since 3.24.

Short summary of the most important changes compared to 3.23 releases

  • generated Java code is now fully generic, including higher kinded types.
  • code for Java8 and above can be generated with lambdas instead of anonymous functions.
  • certain run time classes have been dropped (Algebraic, Applicable) or replaced (Delayed, Lambda) or they are generic now (Lazy).

Frege Run Time System

The Frege Run Time System consists of a handfull of interfaces and abstract classes, which are used to implement laziness, function types and higher kinded types, as explained below. In addition, there are some provisions for IO, parallelism through the Fork-Join framework and concurrency with thread pools. However, none of this requires any initialization, it'll just be there when the Frege code needs it.

The Java7/8 Compatibility Problem

Some of the most important runtime types, like functions, lazy values and thunks exist twice in different packages frege.run7 and frege.run8. The reason is that there is no way to share them between code that uses Java lambdas and code that is compatible with Java7.

This is, for the time being, a dilemma for the author of Java code that calls into Frege. The following solutions are conceivable:

  • Write your Java code inline in Frege. Pro: the Frege compiler will supply the appropriate imports, and the code will run in any Java version. Con: inline code must be written without help from an IDE, and this may become hard. (Workaround: write your code first in a separate Java class, then copy relevant portions to the Frege source)

  • Be future oriented: just forget Java7. Pro: it's the future. Con: Unfortunately, due to long-standing unfixed bugs, javac8 is currently unable to reliably compile Frege generated code (while javac9 compiles the same code without complaints), so this strategy will win only when Java9 becomes the standard.

  • Make a master version for the JVM level you want to support most, but in such a way that you can derive the other version by simply replacing the relevant imports, changing frege.run7 to frege.run8 or vice versa. Pro: you can support all Frege users. Con: higher order functions will be difficult, and if you need them, you must use java7 compatible anonymous classes.

In the following, I write frege.run? when I mean: frege.run7 or frege.run8 depending on your compilation target.

Lazy values: frege.run?.Lazy

The type for modelling lazy values is basically (I show here the Java8 variant):

@FunctionalInterface
public interface Lazy<R> extends Callable<R> {
    /**
     * <p> Compute the value if it is needed. </p>
     * 
     * @see java.util.concurrent.Callable#call()
     */
    public R call();
}

From this, it should be clear, that when you have Lazy<X> and want an X, you need to invoke the call() method.

But quite as often, you have an X , but want a Lazy<X> , for example when you want to pass a value to a Frege function that expects a Lazy<X>. In this case, it is important to know that the Java incarnations of Frege algebraic data types as well as Frege function types implement Lazy (their call() method returns just this), so they can simply be passed where their Lazy form is needed.

The only exception are enumeration types that are mapped to java type short and type renamings of native types. Natives types do not normally implement Lazy , and you have 3 guesses to find out why this is so.

So, how do we make a lazy value? This is the time to introduce Boxes and Thunks.

Thunks and Boxes frege.run?.Thunk frege.run?.Box

While Lazy is just an interface that doesn't specify how call() is implemented, Thunk is a class that implements Lazy and guarantees that the evaluation cost of call() (if any) is incurred at most once, because the result gets cached, waiting for the next call() where the previously computed value is just returned again. In addition, it is guaranteed that this remains true even in the presence of multiple threads, who try to evaluate that value. Only one thread will evaluate the value, which is afterwards available to all threads.

Consider the following example:

(Lazy<Integer>) (() -> /* some expensive computation */)

One can pass this to a function or data constructor that expects a Lazy<Integer> as argument. But every time the function invokes call() on that argument, the expensive computation will run. We can do better by creating a Thunk thus:

Thunk.<Integer>shared( () -> /* some expensive computation */ )

Now, the Thunk evaluation mechanism ensures that /* some expensive computation */ is performed at most once, and the result is shared among all interested parties.

Clearly, Thunk.shared is overkill if we already have a value. We would not want to write

Thunk.<Integer>shared( () -> 42 )

just to place an int in a list or tuple! Instead, the lazy method comes in handy here:

Thunk.<Integer>lazy( 42 )

This will actually create an instance of class Box, another instantiation of Lazy that is specialized on just boxing values.

Take home so far

  • The java type of a lazy value is Lazy<T> if T is the java type of the value.

  • To get the value out of a Lazy, invoke call() on it.

  • Algebraic data types and function types implement Lazy

  • Enumeration types, native types and renamings (newtype) of native/enumeration types do not implement Lazy

  • To get a Lazy<R> of a value with type R, pass the value to Thunk.<R>lazy. This is needed only if R is or could be a type that doesn't implement Lazy.

  • To delay a computation, use

      (Lazy<T>)( () -> /* compute a T */ )
    
  • To create a delayed computation that is evaluated at most once (a thunk), use

      Thunk.<T>shared( () -> /* compute a T */ )
    
  • Don't assume anything about the actual class of a Lazy value! Stick with the Lazy interface and the Thunk.shared and Thunk.lazy operations, and apart from the latter two, forget that Thunk and Box exist.

Where Frege uses Lazy

  • Fields of algebraic datatypes are lazy, unless they are marked strict with a bang. For example, list and tuple elements are lazy.
  • Higher order functions take lazy arguments and return a lazy result.
  • Class operations invoked through an instance dictionary take lazy arguments.
  • Top level functions are compiled to static methods. Their arguments and the return types can be lazy or strict, depending on the judgement of the strictness analyzer. Your IDE can help you identify the actual types.
Clone this wiki locally