BAM Weblog

Haskell on Android using Eta

Brian McKenna — 2017-05-23

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.

Please enable JavaScript to view the comments powered by Disqus.