Foreign Function Interface

Haskell FFI

Note

This section currently only applies to the GHC backend.

The FFI is controlled by five pragmas:

  • IMPORT
  • COMPILED_TYPE
  • COMPILED_DATA
  • COMPILED
  • COMPILED_EXPORT

All FFI bindings are only used when executing programs and do not influence the type checking phase.

The IMPORT pragma

{-# IMPORT HsModule #-}

The IMPORT pragma instructs the compiler to generate a Haskell import statement in the compiled code. The pragma above will generate the following Haskell code:

import qualified HsModule

IMPORT pragmas can appear anywhere in a file.

The COMPILED_TYPE pragma

postulate D : Set
{-# COMPILED_TYPE D HsType #-}

The COMPILED_TYPE pragma tells the compiler that the postulated Agda type D corresponds to the Haskell type HsType. This information is used when checking the types of COMPILED functions and constructors.

The COMPILED_DATA pragma

{-# COMPILED_DATA D HsD HsC1 .. HsCn #-}

The COMPILED_DATA pragma tells the compiler that the Agda datatype D corresponds to the Haskell datatype HsD and that its constructors should be compiled to the Haskell constructors HsC1 .. HsCn. The compiler checks that the Haskell constructors have the right types and that all constructors are covered.

Example:

data List (A : Set) : Set where
  []   : List A
  _::_ : A  List A  List A

{-# COMPILED_DATA List [] [] (:) #-}

Built-in Types

The GHC backend compiles certain Agda built-ins to special Haskell types. The mapping between Agda built-in types and Haskell types is as follows:

Agda Built-in Haskell Type
STRING Data.Text.Text
CHAR Char
INTEGER Integer
BOOL Boolean
FLOAT Double

Warning

Agda FLOAT values have only one logical NaN value. At runtime, there might be multiple different NaN representations present. All such NaN values must be treated equal by FFI calls to avoid making Agda inconsistent.

The COMPILED pragma

postulate f :  a b  (a  b)  List a  List b
{-# COMPILED f HsCode #-}

The COMPILED pragma tells the compiler to compile the postulated function f to the Haskell Code HsCode. HsCode can be an arbitrary Haskell term of the right type. This is checked by translating the given Agda type of f into a Haskell type (see Translating Agda types to Haskell) and checking that this matches the type of HsCode.

Example:

postulate String : Set
{-# BUILTIN STRING String #-}

data Unit : Set where unit : Unit
{-# COMPILED_DATA Unit () () #-}

postulate
  IO       : Set  Set
  putStrLn : String  IO Unit

{-# COMPILED_TYPE IO IO #-}
{-# COMPILED putStrLn putStrLn #-}

Polymorphic functions

Agda is a monomorphic language, so polymorphic functions are modeled as functions taking types as arguments. These arguments will be present in the compiled code as well, so when calling polymorphic Haskell functions they have to be discarded explicitly. For instance,

postulate
  map : {A B : Set}  (A  B)  List A  List B

{-# COMPILED map (\_ _ → map) #-}

In this case compiled calls to map will still have A and B as arguments, so the compiled definition ignores its two first arguments and then calls the polymorphic Haskell map function.

Handling typeclass constraints

The problem here is that Agda’s Haskell FFI doesn’t understand Haskell’s class system. If you look at this error message, GHC complains about a missing class constraint:

No instance for (Graphics.UI.Gtk.ObjectClass xA)
  arising from a use of Graphics.UI.Gtk.objectDestroy’

A work around to represent Haskell Classes in Agda is to use a Haskell datatype to represent the class constraint in a way Agda understands:

{-# LANGUAGE GADTs #-}
data MyObjectClass a = ObjectClass a => Witness

We also need to write a small wrapper for the objectDestroy function in Haskell:

myObjectDestroy :: MyObjectClass a -> Signal a (IO ())
myObjectDestroy Witness = objectDestroy

Notice that the class constraint disappeared from the Haskell type signature! The only missing part are the Agda FFI bindings:

postulate
  Window : Set
  Signal : Set  Set  Set
  MyObjectClass : Set  Set
  windowInstance : MyObjectClass Window
  myObjectDestroy :  {a}  MyObjectClass a  Signal a Unit
{-# COMPILED_TYPE Window Window #-}
{-# COMPILED_TYPE Signal Signal #-}
{-# COMPILED_TYPE MyObjectClass MyObjectClass #-}
{-# COMPILED windowInstance (Witness :: MyObjectClass Window) #-}
{-# COMPILED myObjectDestroy (\_ → myObjectDestroy) #-}

Then you should be able to call this as follows in Agda:

p : Signal Window Unit
p = myObjectDestroy windowInstance

This is somewhat similar to doing a dictionary-translation of the Haskell class system and generates quite a bit of boilerplate code.

The COMPILED_EXPORT pragma

New in version 2.3.4.

g :  {a : Set}  a  a
g x = x

{-# COMPILED_EXPORT g hsNameForG #-}

The COMPILED_EXPORT pragma tells the compiler that the Agda function f should be compiled to a Haskell function called hsNameForF. Without this pragma, functions are compiled to Haskell functions with unpredictable names and, as a result, cannot be invoked from Haskell. The type of hsNameForF will be the translated type of f (see Translating Agda types to Haskell). If f is defined in file A/B.agda, then hsNameForF should be imported from module MAlonzo.Code.A.B.

Example:

-- file IdAgda.agda
module IdAgda where

idAgda : {A : Set}  A  A
idAgda x = x

{-# COMPILED_EXPORT idAgda idAgda #-}

The compiled and exported function idAgda can then be imported and invoked from Haskell like this:

-- file UseIdAgda.hs
module UseIdAgda where

import MAlonzo.Code.IdAgda (idAgda)
-- idAgda :: () -> a -> a

idAgdaApplied :: a -> a
idAgdaApplied = idAgda ()

Translating Agda types to Haskell

Note

This section may contain outdated material!

When checking the type of COMPILED function f : A, the Agda type A is translated to a Haskell type TA and the Haskell code Ef is checked against this type. The core of the translation on kinds K[[M]], types T[[M]] and expressions E[[M]] is:

K[[ Set A ]] = *
K[[ x As ]] = undef
K[[ fn (x : A) B ]] = undef
K[[ Pi (x : A) B ]] = K[[ A ]] ->  K[[ B ]]
K[[ k As ]] =
  if COMPILED_TYPE k
  then *
  else undef

T[[ Set A ]] = Unit
T[[ x As ]] = x T[[ As ]]
T[[ fn (x : A) B ]] = undef
T[[ Pi (x : A) B ]] =
  if x in fv B
  then forall x . T[[ A ]] -> T[[ B ]]
  else T[[ A ]] -> T[[ B ]]
T[[ k As ]] =
  if COMPILED_TYPE k T
  then T T[[ As ]]
  else if COMPILED k E
  then Unit
  else undef

E[[ Set A ]] = unit
E[[ x As ]] = x E[[ As ]]
E[[ fn (x : A) B ]] = fn x . E[[ B ]]
E[[ Pi (x : A) B ]] = unit
E[[ k As ]] =
  if COMPILED k E
  then E E[[ As ]]
  else runtime-error

The T[[ Pi (x : A) B ]] case is worth mentioning. Since the compiler doesn’t erase type arguments we can’t translate (a : Set) → B to forall a. B — an argument of type Set will still be passed to a function of this type. Therefore, the translated type is forall a. () → B where the type argument is assumed to have unit type. This is safe since we will never actually look at the argument, and the compiler compiles types to ().