Introducing Idris on the JVM and an Idris Android Example
Idris on the JVM! Yes, a dependently typed language on the JVM!
I have been working on a JVM bytecode backend for Idris for the past few months and
it is now at a point that we can even write Android programs with Idris without having to write a single line of Java.
In this post, we will see how Idris works on the JVM and an example Android program written in Idris.
Dependencies are provided as Idris packages, not as Java dependencies like jar or class files.
The overall process is that the compiler reads Idris files and converts them into an intermediate JSON representation and
the JVM bytecode generator takes the JSON files and converts them into JVM bytecode class files directly.
It is only when we run a Java class, we have to provide Java dependency jars. The output option -o represents a directory
where the Java class files will be created.
Why do we have all these classes? We only compiled Main module! This is because Idris performs whole program analysis/compilation
and code generator generates bytecode for all the modules that are relevant for the result produced by the main program.
How does Idris JVM handle tail calls?
Idris JVM eliminates self-recursion with JVM GOTO and uses trampolines for other tail calls.
Let’s look at the following examples.
This program would work just fine without blowing up the stack as it will be compiled down to a loop that uses JVM’s GOTO instruction.
Here is the relevant section from bytecode:
The above code also would work fine without killing the stack. Mutual recursion is handled using trampolines and
the tail calls are delayed and compiled down to Java 8 lambdas. As the bytecode for this is bit long, here is the
decompiled bytecode for the evenT function:
As we can see from the decompiled output above, the oddT call is not performed on the same call stack but a thunk
wrapping the function call is returned using lambda (which is compiled down to JVM’s invokedynamic instruction).
Here is the relevant bit from bytecode for those who are interested:
Java interoperability: Calling Java from Idris and calling Idris from Java
Idris JVM supports calling Java static methods, instance methods, constructors and also accessing static and instance fields from Idris.
At the moment, except for Java arrays, all the Java types can be constructed from Idris and passed to Java methods.
Support for handling nulls and exceptions is currently in progress and will soon be available.
(Update 01/10/2017: We can now
use Maybe type to avoid Java nulls
in Idris code)
To use Idris functions from Java, Idris JVM supports exporting Idris functions as static methods, instance methods,
constructors of an exported Java class. The exported class can also extend a Java class or implement interfaces with Idris functions.
To demonstrate these features, let’s create an Android application In Idris.
moduleMainimportIdrisJvm.IOimportJava.Langpythag:Int->List(Int,Int,Int)pythagmax=[(x,y,z)|z<-[1..max],y<-[1..z],x<-[1..y],x*x+y*y==z*z]IdrisAndroidActivity:TypeIdrisAndroidActivity=JVM_Native$Class"hello/IdrisAndroidActivity"Bundle:TypeBundle=JVM_Native$Class"android/os/Bundle"Context:TypeContext=JVM_Native$Class"android/content/Context"View:TypeView=JVM_Native$Class"android/view/View"TextView:TypeTextView=JVM_Native$Class"android/widget/TextView"InheritsViewTextViewwhere{}CharSequence:TypeCharSequence=JVM_Native$Class"java/lang/CharSequence"InheritsCharSequenceStringwhere{}superOnCreate:IdrisAndroidActivity->Bundle->JVM_IO()superOnCreate=invokeInstance"superOnCreate"(IdrisAndroidActivity->Bundle->JVM_IO())getApplicationContext:IdrisAndroidActivity->JVM_IOContextgetApplicationContext=invokeInstance"getApplicationContext"(IdrisAndroidActivity->JVM_IOContext)newTextView:Context->JVM_IOTextViewnewTextView=FFI.new(Context->JVM_IOTextView)setText:InheritsCharSequencecharSequence=>TextView->charSequence->JVM_IO()setTextthistext=invokeInstance"setText"(TextView->CharSequence->JVM_IO())this(believe_metext)setContentView:InheritsViewview=>IdrisAndroidActivity->view->JVM_IO()setContentViewthisview=invokeInstance"setContentView"(IdrisAndroidActivity->View->JVM_IO())this(believe_meview)onCreate:IdrisAndroidActivity->Bundle->JVM_IO()onCreatethisbundle=dosuperOnCreatethisbundlecontext<-getApplicationContextthistextView<-newTextViewcontextsetTexttextView$"Hello Android from Idris! pythag 50 is "++show(pythag50)setContentViewthistextViewmain:IO()main=pure()androidExport:FFI_ExportFFI_JVM"hello/IdrisAndroidActivity extends android/support/v7/app/AppCompatActivity"[]androidExport=FunsuperOnCreate(Super"onCreate")$FunonCreate(ExportInstance"onCreate")$End
The above program demonstrates calling Java instance methods (setText for example) and constructors (newTextView).
It further demonstrates how to handle inheritance relationship when passing subclass instances to a parent class type.
For example, function setContentView takes a View but we can pass a TextView as it is a subclass of View and we
mention that to Idris via Inherits View TextView where {}.
It also demonstrates how we can create a Java class that extends another class and override methods with Idris functions.
The last section androidExport creates a Java class named hello/IdrisAndroidActivity that extends android/support/v7/app/AppCompatActivity.
The Java class also creates a wrapper method superOnCreate that just delegates to super.OnCreate and the class also overrides onCreate method
with Idris' onCreate function. The Java class can also implement one or more Java interfaces with something like,
Then package the classes compiled above along with idris-jvm-runtime-1.0-SNAPSHOT.jar classes in a single jar and copy into an
android project’s app/libs directory.
Change the activity class name in android manifest file to the Idris exported class name hello.IdrisAndroidActivity.
Then run ./gradlew installDebug from android project after starting an emulator or connected to an android device.
Finally we should see our Idris code running on Android! It should look something like this: