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.6k stars 141 forks source link

allow JWT types to provide instances to typeclasses #243

Open csierra opened 7 years ago

csierra commented 7 years ago

Some Java classes present Functor or Monad shapes (like java.util.Optional). It would be great to provide instances for Functor or Monad for this types that leverage their native implementations.

For example:

{-# LANGUAGE MagicHash, FlexibleContexts, TypeFamilies, DataKinds#-}

import Java
import Control.Monad

data {-# CLASS "java.util.Optional" #-} Optional a = Optional (Object# (Optional a))
  deriving (Class, Eq, Show)

data {-# CLASS "java.util.Function" #-} Function a b =
  Function (Object# (Function a b))

foreign import java unsafe "@wrapper apply"
  fun :: (Extends t Object, Extends r Object) => (t -> Java (Function t r) r) -> Function t r

foreign import java unsafe "@static java.util.Optional.of"
  oof :: (Extends t Object) => t -> Optional t

foreign import java unsafe get :: (Extends a Object) => (Optional a) -> a
foreign import java unsafe jmap :: (Extends a Object) => (Optional a) -> Function a b -> Optional b

jfmap :: (Class a, Class b) => (a -> b) -> Optional a -> Optional b
jfmap f a = a `jmap` (fun  (return . f))

instance Functor Optional where
  fmap = jfmap

currently this is not possible due to the type constraint (Class a, Class b) which makes fmap implementation less polymorphic than the one expected by Functor

rahulmutt commented 7 years ago

As we discussed via Gists, the best approach is to have JWT-specific typeclasses like JMonad, JApplicative, and JFunctor. Once you've played around with it, and you'll figured out all the functionality you need for your use-cases, we can collect all the definitions into a Java.Do module and add it to the standard library. Once it's been added, I'll close this issue.

Adding this to the v0.1.0 milestone.

csierra commented 7 years ago

hey @rahulmutt... thanks so then there would be no other trick/magic to circumvent the (Class a) constraint?

If I understand it correctly this all happens because of the restriction of wrapping a GHC function into a java Function using @wrapper. Wouldn't be any smoother way of implementing a java Function from a haskell one?

rahulmutt commented 7 years ago

Nope, unless I add special support for it and I don't think that would be a good idea, and here's why:

Take the following code as an example:

data SomeInteger = SomeInteger Int

f :: SomeInteger -> SomeInteger
f (SomeInteger i) = i'
   where i' = SomeInteger $ inc i
         inc x = x + 1

Does it make sense to translate f directly to Function SomeInteger SomeInteger? No, because the function f is lazy. It returns a thunk. A Function SomeInteger SomeInteger should be strict in its argument and return type by Java semantics.

Moreover, the Java-level representation of SomeInteger is not your typical Java object since it is designed to account for laziness. Due to these semantic differences between Java and Eta (particularly due to laziness vs strictness), we only allow you to create a Function (or any interface) only from datatypes we know will be simple, non-lazy objects.

In this case, the Class constraint guarantees that for us. JWTs are simple wrappers around Java objects and are strict in the wrapping (meaning you must have an evaluated Java object inside at all times) and hence we know we can safely pass them between boundary of Eta and Java without mismatch in semantics.

I can imagine that this boundary will cause annoyances at times (we observed this when working with Apache Spark in Eta) but I think we can find ways to make it more convenient without loss of safety with some type system features. This boundary is what allows us to safely interact with the Java world from a purely functional language like Eta.

If you have specific use cases where you want to relax this restriction and the translation toFunction still makes sense, please share them and we can work out how to accommodate those cases in a more general framework.

csierra commented 7 years ago

It seems I bumped into another issue. When jfmap or jflatMap methods return an interface (like CompletionStage I get the following runtime error:

Exception in thread "main" java.lang.IncompatibleClassChangeError: Found interface com.liferay.functional.validation.Validation, but class was expected
    at main.Main$zdLr8D6zdcjbind.enter(Unknown Source)
    at main.Main$satzus8JV.enter(Unknown Source)
    at eta.runtime.apply.Apply$PAPApply.enter(Apply.java:231)
    at eta.runtime.apply.StgPAP.apply(StgPAP.java:229)
    at eta.runtime.apply.ApPP.stackEnter(ApPP.java:19)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:43)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:26)
    at eta.runtime.stg.StgContext.checkForStackFrames(StgContext.java:79)
    at main.Main$zdLr8D0zdcshowsPrec.enter(Unknown Source)
    at eta.runtime.apply.StgFun.apply(StgFun.java:291)
    at eta.runtime.apply.ApPPP.stackEnter(ApPPP.java:21)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:43)
    at eta.runtime.stg.StgContext.checkForStackFrames(StgContext.java:79)
    at base.ghc.io.handle.Text$zdwa7.enter(Unknown Source)
    at base.ghc.io.handle.Text$hPutStr2.enter(Unknown Source)
    at base.system.IO$print1.enter(Unknown Source)
    at base.system.IO$print.enter(Unknown Source)
    at eta.runtime.apply.Apply$PAPApply.enter(Apply.java:231)
    at eta.runtime.apply.StgPAP.apply(StgPAP.java:46)
    at eta.runtime.apply.ApV.stackEnter(ApV.java:12)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:43)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:26)
    at eta.runtime.stg.StackFrame.enter(StackFrame.java:26)
    at eta.runtime.stg.Capability.schedule(Capability.java:260)
    at eta.runtime.RtsScheduler.scheduleWaitThread(RtsScheduler.java:57)
    at eta.runtime.Rts.evalLazyIO(Rts.java:95)
    at eta.runtime.Rts.hsMain(Rts.java:38)
    at eta.main.main(Unknown Source)
csierra commented 7 years ago

as @Jyothsnasrinivas pointed out, I failed to add "@interface" to the method declaration. Doing so solves the problem.

rahulmutt commented 6 years ago

@csierra I've been reconsidering this and I think it's safe to store Eta types inside of Java generic containers (like Optional), given the conversation here: https://github.com/typelead/eta/issues/690