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:
Write Haskell for the data type we wish to read in
Allow Dhall to generate an
Interpret
instance.Allow ats-storable to generate a
Storable
instance.Allow hs2ats to generate ATS types based on the Haskell we wrote.
Write the boilerplate for memory management and testing in ATS.
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.