typelead / eta

The Eta Programming Language, a dialect of Haskell on the JVM
https://eta-lang.org
BSD 3-Clause "New" or "Revised" License
2.61k stars 141 forks source link

how to export nullable data type to java? #954

Open clojurians-org opened 5 years ago

clojurians-org commented 5 years ago

i export an eta object to java for implementing interface. but the interface result object can be null on some situation. i didn't find any eta code for doing this, so what i should do for this case?

rahulmutt commented 5 years ago

@clojurians-org Have you tried using Maybe [object] as your return type? Nothing should get marshalled to null.

clojurians-org commented 5 years ago

thanks, i'm trying do it. but another issue occur, i don't know to build the object for export.

data HTest = HTest @my.HTest deriving Class
foreign export java mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = return $ Just HTest
    Expected type: Int -> Java a (Maybe HTest)
      Actual type: Int
                   -> Java
                        a (Maybe (ghc-prim-0.4.0.0:GHC.Prim.Object# HTest -> HTest))
jneira commented 5 years ago

Hi! I think you have to create an instance of HTest instead, somethink like:

foreign import java unsafe "@new" newHTest  :: Java a HTest
mkHTest _ = return $ Just newHTest
clojurians-org commented 5 years ago

it seems it didn't generate the corresponding class file:

the code is very simple:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest
foreign export java mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

unzip jar, and find it.

[op@my-200 tmp]$ jar -tf callJava.jar | grep HTest.class
main/main/datacons/HTest.class
main/main/tycons/HTest.class
main/Main$$fe_mkHTest.class
main/Main$DHTest.class
main/Main$mkHTest.class
main/Main$newHTest.class

[op@my-200 tmp]$ javap ./main/main/datacons/HTest.class
Compiled from "Main.hs"
public final class main.main.datacons.HTest extends main.main.tycons.HTest {
  public my.HTest x1;
  public main.main.datacons.HTest(my.HTest);
  public int getTag();
}
[op@my-200 tmp]$ javap ./main/main/tycons/HTest.class
Compiled from "Main.hs"
public abstract class main.main.tycons.HTest extends eta.runtime.stg.DataCon {
  public main.main.tycons.HTest();
}
jneira commented 5 years ago

Oh, yeah, you can't use a free type variable in exports if they are not static so i think that:

-- To make eta create the class you have to replace `a` with `HTest`
foreign export java mkHTest :: Int -> Java HTest (Maybe HTest)

... should work.

However in the java world it is not frequent to use a instance method to generate an instance of the same class but a static factory method so this one maybe would be more idiomatic:

data HTest = HTest @my.HTest deriving Class

-- As all classes have an empty constructor by default you can import it.
foreign import java unsafe "@new" newHTest  :: Java a HTest

-- This export actually creates the class my.HTest
foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

Note that you can use Java a (Maybe HTest) in this case cause the export has an @static annotation. It is a little bit tricky but i hope the info in the user guide could help you.

There is plans to make exports automatic to avoid all this: #690

clojurians-org commented 5 years ago

it seems the maybe type didn't handle null value well. this is the information from my side

larrys-MBP:javaCall larluo$ cat src/Main.hs
module Main where

import GHC.Base(Class)
import Java (Java)

main :: IO ()
main = putStrLn "Hello, Eta!"

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

foreign export java "@static my.HTest.mkHTest2" mkHTest2 :: Int -> Java a HTest
mkHTest2 _ = newHTest
larrys-MBP:tmp larluo$ javap my/HTest.class 
public class my.HTest {
  public my.HTest();
  public static my.HTest mkHTest2(int);
  public static eta.runtime.stg.Closure<my.HTest> mkHTest(int);
}
wget https://download.clojure.org/install/clojure-tools-1.10.0.442.tar.gz
larrys-MBP:javaCall larluo$ java -cp clojure-tools-1.10.0.442.jar:./dist/build/eta-0.8.6.5/javaCall-0.1.0.0/x/javaCall/build/javaCall/javaCall.jar clojure.main
Clojure 1.10.0
user=> (import '[my HTest])
my.HTest
user=> (HTest/mkHTest2 1)
#object[my.HTest 0x45f24169 "my.HTest@45f24169"]
user=> (HTest/mkHTest 0)
Execution error (NoSuchFieldError) at my.HTest/mkHTest (REPL:-1).
x1
user=>  (HTest/mkHTest 1)
Execution error (ClassCastException) at my.HTest/mkHTest (REPL:-1).
base.ghc.maybe.datacons.Just cannot be cast to base.ghc.maybe.datacons.Nothing
rahulmutt commented 5 years ago

@jneira I remember we allowed arbitrary closures in the foreign export mechanism a while back. I wonder if we need to fix that to handle Maybe's properly.

jarekratajski commented 5 years ago

I am just analyzing this case. For a very simple file:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest4" mkHTest4 :: Int -> Java a (Maybe HTest)
mkHTest4 _ = return Nothing

I am wondering why the final cast in method bytecode is to Nothing (not to Maybe). So far I got that typeDataConClass is DsForeign.hs for Type argument Maybe HTest returns a class name: base/ghc/maybe/datacons/Nothing Which seems odd, and is (maybe :-) ) one of the problems.

I am going further.

jarekratajski commented 5 years ago

Hi! I've created somehow naive but working solution. I catch explicitly Maybe result types in export FFI and make check for Notihng.INSTANCE - in case of eq there is null returned, otherwise code as before. After some code cleanup I could potentially make PR. I am however, awaiting for hints / issues. (I've used the example to relearn again eta/ghc compiler - very likely some fragments are reinventing the wheel. Also I am not even sure if the whole approach is sensible).

Fix works with such code:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return $  Nothing
mkHTest _ = Just <$> newHTest
        Closure obj = HTest.mkHTest(1);
        System.out.println(obj.getClass());
        System.out.println(obj);

        Closure obj2 = HTest.mkHTest(0);
        System.out.println(obj2);
rahulmutt commented 5 years ago

@jarekratajski Just took a look at your fork and it looks good to me. Feel free to send a PR.

Make sure to add a couple tests. The testing framework was upgraded and it now supports full etlas projects so you can make a project that uses both Eta/Java to make sure this is working properly.

jarekratajski commented 5 years ago

I have realised that this result as Closure<X> in exported Java is probably not something anyone would want - I am trying to reduce it to be exactly X (return type in case of Java a (Maybe X). It will take some time - but I am getting there (slowly).