Haskell on Android using Eta
Eta is a fork of GHC which provides Haskell with a JVM backend. I've been working on it recently and did a presentation on it at LambdaJam. One of the questions from my presentation was "since Eta takes Haskell and produces JVM code, can I use it to write Android apps?"
I had a feeling Eta was close to being able to. It turns out it's not just close, it's pretty easy!
Let's write some code which uses the Android API:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Xyz.Profunctor.Android where
import Java
import Java.StringUtils as S
data {-# CLASS "android.content.Context" #-}
Context = Context (Object# Context)
deriving Class
data {-# CLASS "android.app.Activity" #-}
Activity = Activity (Object# Activity)
deriving Class
type instance Inherits Activity = '[Context]
data {-# CLASS "android.view.View" #-}
View = View (Object# View)
deriving Class
data {-# CLASS "android.widget.TextView" #-}
TextView = TextView (Object# TextView)
deriving Class
type instance Inherits TextView = '[View]
foreign import java unsafe "@new"
newTextView :: (c <: Context) => c -> Java a TextView
foreign import java unsafe "setContentView"
setContentView :: (v <: View) => v -> Java Activity ()
foreign import java unsafe "setText"
setText :: (c <: CharSequence) => c -> Java TextView ()
data {-# CLASS "xyz.profunctor.android.ActivityImpl" #-}
ActivityImpl = ActivityImpl (Object# ActivityImpl)
foreign export java "@static startActivity"
startActivity :: Activity -> Java ActivityImpl ()
startActivity :: Activity -> Java a ()
startActivity activity = do
textView <- newTextView activity
textView <.> setText (foldr S.concat "World!" (replicate 10 "Eta "))
activity <.> setContentView textView
This shows some of the power of Eta's foreign function interface. We're able to describe the Android Java interface with some data types representing classes, a type family for representing subtyping and foreign imports representing methods.
The code uses some normal Prelude code (e.g. foldr
, replicate
) to
demonstrate that it's full Haskell.
The above file can be compiled:
$ eta Android.hs
And we get an Android.jar
file!
Now using Android Studio we can create a new project and copy the above
JAR to the app/libs
directory.
We also need the dependencies for this code. Ideally we'd be able to
use Eta's build tool, etlas
, to bundle the dependencies in our JAR
(via etlas configure --enable-uberjar-mode
) but sadly it doesn't yet
support the Uber JAR mode for code without a Main
module.
So let's work around the problem for now. Some dodgy shell to copy the dependency files:
for library in ghc-prim base rts integer; do
DIR="$(eta-pkg field $library library-dirs --simple-output)"
JAR="$(eta-pkg field $library hs-libraries --simple-output).jar"
cp "$DIR/$JAR" app/libs
done
Now we can edit the project's Java code to use the exported Haskell method:
package xyz.profunctor.etaandroid;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import xyz.profunctor.android.ActivityImpl;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityImpl.startActivity(this);
}
}
Doing a debug build will try to create an APK but will fail because we have an enormous number of methods. To work around this, we can enable Multidex, which is supported since Android API level 21 (i.e. at least Android Lollipop 5.0, around 70% of the devices hitting Google Play) - getting Proguard to run during debug builds should work instead, but I didn't bother because enabling Multidex was easy.
We need to merge the following settings with the generated
app/build.gradle
:
android {
defaultConfig {
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled true
}
}
lintOptions {
disable 'InvalidPackage'
}
}
And add these Proguard rules to app/proguard-rules.pro
:
-dontwarn base.**
-dontwarn eta.**
-dontwarn ghc_prim.**
-dontwarn integer.**
-dontwarn main.**
Without Proguard we'll get a 5MB APK file. After Proguard it should be only around 900KB.
Pressing the buttons in Android Studio will start the app up in the
emulator, or we can compile using gradle
:
$ gradle assembleDebug # for a debug build
$ gradle assembleRelease # for release build
$ gradle build # do all the things
Here's what it looks like under Android Studio's emulator:
And here's a screenshot from a physical Android device!
I've chucked the code up for the Android Studio part of the
code which just needs the
app/libs
directory to be made.