As astute followers of this blog will know, I have been working on several ATS packages for Haskell; here I would like to present the fruits of these labors: the ability to use Haskell's Dhall library in ATS.

Motivation: Dhall in ATS

There are two reasons one would want to write Dhall bindings for ATS. The first is that ATS is a relatively obscure language and thus has relatively few libraries available. Bindings C libraries is a common way to write ATS, but as ATS is an effective systems programming language on its own, we can really bind to anything we want.

The second is that ATS has a relatively advanced type system. In practice, any type expressible in Haskell will be expressible in ATS (though not conversely). This is not the case with Rust, which does not have universally quantified types. It is well worth exploring what systems programming looks like with modern techniques, in particular passing algebraic data types between programs.

Example

Our example will not provide the full generality library bindings would. However, as ATS does not have generics, it will end up suiting our needs quite well (with the minor downside of a polyglot project).

We will proceed as follows:

This is no doubt less pleasant than using a library in a language such as Rust, but those familiar with ATS will note that this is pretty standard; offloading the hard work to generics requires us to use an entirely different language. Moreover, neither Rust nor Scala bindings for Dhall are complete, so this is in fact unique to the ATS ecosystem.

Let's first examine the Dhall expression to be parsed:

let Option = < Some : { _1 : { first : Integer, second : Integer } } | None : {} >
in
let None = < Some : { _1 : { first : Integer, second : Integer } } | None = {=} >
in
let Some = λ(x : { first : Integer, second : Integer })  < Some = { _1 = x } | None : {} >
in
let p = { first = 1, second = 6 }
in

Some p

This is a bit abstruse, but it becomes a little clearer when we look at the Haskell:

data Option a = Some a
              | None
              deriving (Generic, Interpret, Functor, ATSStorable, Data)

data Pair a b = Pair { first :: a, second :: b }
    deriving (Generic, Interpret, ATSStorable, Data)

The Haskell is pleasantly compact, though mostly trivial:

{-# LANGUAGE DeriveAnyClass           #-}
{-# LANGUAGE DeriveDataTypeable       #-}
{-# LANGUAGE DeriveFunctor            #-}
{-# LANGUAGE DeriveGeneric            #-}
{-# LANGUAGE FlexibleInstances        #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings        #-}

module Foreign where

import           Data.Bifunctor
import           Data.Data
import qualified Data.Text.Lazy       as TL
import           Dhall
import           Foreign.C.String
import           Foreign.C.Types
import           Foreign.Ptr
import           Foreign.Storable.ATS

data Option a = Some a
              | None
              deriving (Generic, Interpret, Functor, ATSStorable, Data)

data Pair a b = Pair { first :: a, second :: b }
    deriving (Generic, Interpret, ATSStorable, Data)

instance Bifunctor Pair where
    bimap f g (Pair x y) = Pair (f x) (g y)

type Product = Pair CInt CInt

readDhall :: FilePath -> IO (Option Product)
readDhall p = fmap (bimap g g) <$> input auto (TL.pack p)
    where g :: Integer -> CInt
          g = fromIntegral

read_dhall :: CString -> IO (Ptr (Option Product))
read_dhall cStr = do
    str <- peekCString cStr
    x <- readDhall str
    writePtr x

foreign export ccall read_dhall :: CString -> IO (Ptr (Option Product))

On the ATS side of things, we get the following generated code (from hs2ats):

datavtype option(a: vt@ype) =
  | Some of a
  | None

vtypedef pair(a: vt@ype, b: vt@ype) = @{ first = a, second = b }
vtypedef product = pair(int, int)

This will be our handwritten code:

%{^
#define STUB_H "hs/Foreign_stub.h"
#define STG_INIT __stginit_Foreign
%}

#include "$PATSHOMELOCS/hs-bind-0.4.1/runtime.dats"
#include "share/atspre_staload.hats"
#include "share/atspre_staload_libats_ML.hats"

staload "libats/ML/SATS/string.sats"
staload UN = "prelude/SATS/unsafe.sats"
staload "gen/types.sats"

fun free_option(x : option(pair(int,int))) : void =
  case+ x of
    | ~Some (x) => ()
    | ~None() => ()

fun tostring_option_pair(x : !option(pair(int,int))) : string =
  case+ x of
    | None() => "None"
    | Some (x) => let
      var f = x.first
      var s = x.second
    in
      string_append5( "Some (Pair { first = "
                    , tostring_int(f)
                    , ", second = "
                    , tostring_int(s)
                    , " })"
                    )
    end

extern
fun hs_read(string) : ptr =
  "mac#read_dhall"

implement main0 (argc, argv) =
  {
    val _ = hs_init(argc, argv)
    var x = $UN.ptr0_get<option(pair(int,int))>(hs_read("./example.dhall"))
    val s = tostring_option_pair(x)
    val _ = println!(s)
    val _ = free_option(x)
    val _ = hs_exit()
  }

And finally our configuration (first in Dhall for atspkg, then a .cabal file):

let pkg = https://raw.githubusercontent.com/vmchale/atspkg/master/ats-pkg/dhall/default.dhall
in
let dbin = https://raw.githubusercontent.com/vmchale/atspkg/master/ats-pkg/dhall/default-bin.dhall

in pkg //
  { test =
    [
      dbin //
      { src = "src/dhall-ats.dats"
      , target = "target/dhall-ats"
      , hsDeps = [ { cabalFile = "hs/foreign.cabal", objectFile = "hs/Foreign.o", projectFile = ([] : Optional Text ) } ]
      , hs2ats = [ { hs = "hs/Foreign.hs", ats = "gen/types.sats", cpphs = False } ]
      }
    ]
    , dependencies = [ "hs-bind" ]
    , ccompiler = "ghc"
    , cflags = ["hs/Foreign"]
  }
name: foreign
version: 0.1.0.0
cabal-version: >= 1.2
build-type: Simple

library
  build-depends: base < 5
               , dhall
               , filepath
               , ats-storable
               , text
  exposed-modules: Foreign
  hs-source-dirs: .
  ghc-options: -Wall -Werror -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat

This of course assumes you name your Haskell source file Foreign.hs and your ATS source src/dhall-ats.dats. You can see here for the source code if you have any trouble getting this up and running.

You should be able to now run

atspkg test

and see the result! If all went well, you should see

Some (Pair { first = 1, second = 6 })

indicating that our FFI is working correctly.

According to polyglot, our project contents are as follows:

-------------------------------------------------------------------------------
 Language             Files       Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 ATS                      1          45           40            0            5
 Cabal                    1          14           13            0            1
 Dhall                    1          18           17            0            1
 Haskell                  1          44           35            0            9
-------------------------------------------------------------------------------
 Total                    4         121          105            0           16
-------------------------------------------------------------------------------

which is certainly less pleasant than the few lines of Haskell, but nonetheless unburdensome compared to other solutions in ATS.