-
Notifications
You must be signed in to change notification settings - Fork 142
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
[eta] Use Eta ADTs from the Java side #690
Comments
So I think this becomes easier if you take a look at how Eta generates the declarations above, we should eventually add what I write below in the docs: Suppose this module occured in a package named module A.B.C where
data Event
= ItemArrived String
| ItemLeft String generates: package event.a.b.c.tycons;
import eta.runtime.stg.DataCon;
public abstract class Event extends DataCon {} package event.a.b.c.datacons;
import event.a.b.c.tycons.Event;
public class ItemArrived extends Event {
public Closure x1; //Corresponds to the String
// In x1, you can store an evaluated list OR you can store a *thunk* which eventually returns a list.
public ItemArrived(Closure x1) {
this.x1 = x1;
}
} Note that successive fields in an ADT are consistenly named Now the question becomes, how do I construct lists since you need to supply a data [a] = [] | a : [a] Is pseudo syntax for how you would declare it. It goes through a process called Z-encoding to get the correspond Java method name. This declaration occurs in import ghc_prim.ghc.types.datacons.ZMZN; // [] = ZMZN w/ Z-encoding
import ghc_prim.ghc.types.datacons.ZC; // : = ZC w/ Z-encoding
import ghc_prim.ghc.Types; // Exposes all the exported methods of GHC.Types in Z-encoded form as static methods
import event.a.b.c.tycons.Event;
import event.a.b.c.datacons.ItemArrived;
ZC string = new ZC(new Czh(48), new ZC(new Czh(49), Types.DZMZN()));
// Corresponds to 48 : (49 : []) = [48, 49] or using as ASCII table, "01"
Event e = new ItemArrived(string); You may have noticed that I used Now let's say you wanted to call this method directly: In package module A.B.C where
emit :: Event -> IO ()
emit = write eventStream Generates: package event.a.b;
import eta.runtime.stg.Closure;
import eta.runtime.stg.StgContext;
public C {
public static Closure emit(StgContext context, Closure x1) {
// Implementation generated by the Eta compiler
}
} You can call this from Java with the import event.a.b.C;
import eta.runtime.Runtime;
Closure result = C.emit(Runtime.getContext(), e);
// In this case, you can ignore the result since it will be of type unit, or an instance of Z0T (Z-encoding for 0-element tuples) NOTE: Calling Eta functions directly by passing in the context like that will work only for pure functions and functions that don't throw exceptions or do concurrency. For example, above you would have done: Closure result = Runtime.evalIO(new Ap2Upd(C.emit(), e)); An Now you don't have to do any guess work or know Eta internals to remember all these rules. Compile some Eta code, and then decompile it so that it becomes easier to see the generated interface and work accordingly. Hope that helps! I know that's a lot to keep in mind, hopefully we'll find ways to make this process easier. Or maybe it's just a matter of documenting it all and putting javadocs for the Runtime helper functions. |
Thank you very much @rahulmutt ! Yes, it actually does look like something not trivial.
If its related to #601 , I'll probably start working on 2 first, as I'm trying to push Eta for an internal project in my company and it is required that it can be used from Java 😄 |
+1 to the @export anotation for data types, to make imports/exports balanced (cause this way you can import and export, methods/functions and data types) |
@NickSeagull Just it case you missed the comment in the wall of text I posted: foreign exports do exactly that - construct the thunk to and execute the expression with Suppose we wanted to export this: emit :: Event -> IO ()
emit = write eventStream
foreign export java unsafe "@static [some-class].emit"
emit :: Event -> IO () so that the generated signature is: public static void emit(Event e); This is compeletely doable, if we assumed that in the But there will be cases where you'd want to be able to supply deferred values and creating your own Thunk is a bit cumbersome if you directly extend from the internal Runtime classes. For that, we can make a helper emit :: Event -> IO ()
emit = write eventStream
foreign export java unsafe "@static [some-class].emit"
emit :: Lazy Event -> IO () so that the generated signature is: public static void emit(Closure a); This is flexible in that it accepts both NOTE: We can add generics to the eta internal types to make things more typesafe, I played around with this here. So the signature above could be: public static void emit(Closure<Event> a); Notice that the Here's how package eta.runtime.Runtime;
import java.util.concurrent.Callable;
import eta.runtime.stg.Value;
public abstract class LazyValue<A extends Value> extends UpdatatableThunk {
public abstract A call();
@Override
public final Closure thunkEnter(StgContext context) {
return call();
}
} So now creating a lazy Event value is as simple as: emit(new LazyValue<Event>() {
@Override
public Event call() {
// Do some deferred stuff here.
}
}); We can also make a version of All this is relatively easy to implement right now with little change - we just need to reduce the strictness of typechecking for foreign exports to allow any type to be valid in the FFI. The foreign export generator (in With the new @Export(class="[some-class]")
emit :: Event -> IO ()
emit = write eventStream @Export(class="[some-class]")
emit :: @Lazy Event -> IO ()
emit = write eventStream Note that if we get annotation support, we can avoid the newtype altogether and use a special |
@jneira Let's play around with how that might work. data Event
= ItemArrived String
| ItemLeft String So now the question is, when exporting, we need to decide that Java type to give @Export
data Event
= ItemArrived (@Type(JString) String)
| ItemLeft (@Type(CharSequence) String) This looks a lot nicer with record notation: @Export(package="[some-package]")
data Event
= ItemArrived { payload :: @Type(JString) String }
| ItemLeft { payload :: @Type(CharSequence) String } Where OR maybe we just make it so that you are only allowed to put JWTs as fields in a type (or Eta types that have the @Export(package="com.somecompany")
data Event
= ItemArrived { payload :: JString }
| ItemLeft { payload :: JString, payload2 :: CharSequence } will generate: package com.somecompany;
public abstract class Event {
// When we have sum-types + record notation with common record selectors,
// we define them up in the parent class.
// Notice our conversion of generating Java getters too.
public abstract getPayload();
}
public class ItemArrived extends Event {
String x1;
public ItemArrived(String x1) {
this.x1 = x1;
}
public String getPayload() {
return x1;
}
}
public class ItemLeft extends Event {
String x1;
CharSequence x2;
public ItemLeft(String x1, CharSequence x2) {
this.x1 = x1;
this.x2 = x2;
}
public String getPayload() {
return x1;
}
public CharSequence getPayload2() {
return x2;
}
} When foreign exporting an Eta type that has an For fields of the form I didn't mention this but all field values for Exported classes are assumed to be strict. |
Yep completely missed it, thanks again for your help Rahul😁 |
@jneira I just realized I didn't address your question about "nice names". Take for example the built-in list type: data [a] = [] | a : [a] The Z-encoding is scary: @Export(package="eta.util")
data @Name("List") [a]
= @Name("Nil") []
| (@Name("Cons") (:)) a [a] So we would have |
@rahulmutt mmm, i thought those names would be automatically generated from haskell names if the type has the |
Yes, so we can enable the This same thing is also useful for value-level operators: @Export { package="eta.util", class="Utils" }
@Name "append"
(++) :: [a] -> [a] -> [a] |
Should we treat these special annotations as keywords? I'm not sure if it would make sense not doing so, as they only make sense in the Eta Realm 🤔 |
@NickSeagull I think these can be actual annotations in say the The problem with keywords becomes - how do I specify that I want to use the |
Maybe we can omit the For example @GetMapping "/user/{id}"
@Export { package="eta.util", class="Utils" }
foo :: Int -> IO Int
foo _ = return 42 makes sense, but probably @GetMapping "/user/{id}"
@Export
foo :: Int -> IO Int
foo _ = return 42 doesn't, as there is no possibility of using an annotation function if it is not exported, so we could leave it like @GetMapping "/user/{id}"
foo :: Int -> IO Int
foo _ = return 42 which is more clear and it is not cluttered by the Another thing we might have in mind is the @Static
@Export { package = "eta.util", class = "Utils" }
bar :: Int -> String
bar _ = "Hi" would be replaced by @Static { package = "eta.util", class = "Utils" }
bar :: Int -> String
bar _ = "Hi" Or even, we could call the annotation |
Another use case: i would like to export already existent ADTs from a haskell package, in my case the ADT representing the AST of dhall lang @Export(package="[some-package]") Dhall.Core.Expr And being the ADT: data Const = Type | Kind
data Expr s a
-- | > Const c ~ c
= Const Const
-- | > Var (V x 0) ~ x
-- > Var (V x n) ~ x@n
| Var Var
-- | > Lam x A b ~ λ(x : A) -> b
| Lam Text (Expr s a) (Expr s a)
....
deriving (Functor, Foldable, Traversable, Show, Eq, Data) I would like to have: public abstract class Const {}
public final class Type extends Const {
public Type() {}
}
....
public abstract class Expr<S,A> {
// A gigantic church encoding of sum types ????, i am afraid it is unusable
// it could be useful for Maybe or Either though
public <T> match(Function<Const,T> f1, Function<Lam<S,A>,T> f2, .... another 50 params) {
}
}
public final class Lam<S,A> extends Expr<S,A> {
private final Text t;
private final Expr<S,A>e1,e2;
public Lam(Text t,Expr<S,A> e1, Expr<S,A> e2) {
...
}
// No sensible default field names (x1,x2,x3??) for product types
// so using a poor man's pattern matching
public <R> match(Function3<Text,Expr<S,A>,Expr<S,A>,R> f) {
...
}
// Not sure about adding type class methods as java methods
public LAM<T,A> fmap (Function<S,T> f1) {
...
}
// same for traverse, and other type classes?
} Moreover we could mark a module as exportable: @ExportModule(package="[some-package]") Some.Module.IncludingThis and exports all its public ADTs automatically if possible @rahulmutt maybe i am asking for something impossible or not practical 😐 |
@jneira Thanks for presenting a nice use case! I like how you handled Church encoding and typeclass methods. But the main issue with the Church encoding you presented is that it can have a large number of arguments. I wonder if for the Java side, it's better to present a
That last call to |
Hi, after writing https://github.com/eta-lang/dhall-eta i did feel the pain to to do a lib that esentially creates a java binding for a haskell lib. It would be very nice that the tool implemented to generate java classes from haskell code will generate automatically those instances. |
I'm currently building a simple library to try stuff with Events.The idea is to have the library written in Eta, while allowing users to use it from Java.
A simplified version of the library could be something like this
NOTE:
Chan
is used here just as an example, I'll use either Kafka, RabbitMQ or others.The idea would be that a user of this library could use it from the Java side in a form like this for emitting events
And in another place, someone could subscribe to it, for example, by implementing an interface.
I don't mind losing type safety as long as I can get the data from the ADT. I can build some helper functions on top of that 😄
The text was updated successfully, but these errors were encountered: