ATS is an incredibly apt systems programming language; as it compiles to C it enjoys all the versatility that C does. In particular, this means that we get some flexibility from the ability to import from high-level languages as well as export to them.

This method is particularly appealing when writing ATS, as ATS has a sparse library support. For a complex task such as making an interpreter for Dhall, writing an entire ATS library is simply infeasible. While the approach contained here is somewhat clumsy in places, it's guaranteed to be correct and up-to-date even for complex tasks.

# Tooling

Once again we'll be using pi and atspkg for our builds. You can look at the post here for more details on installing them.

We'll initialize a project with

pi git vmchale/ats-haskell aeson-demo
cd aeson

# Configuration

Configuration for this build will be pretty simple due to using the template. We just have to add a couple dependencies to our .cabal file. Open up hs/aeson-demo.cabal. You should see the following:

name: aeson-demo
version: 0.1.0.0
cabal-version: >= 1.2
build-type: Simplelibrary
build-depends: base < 5
, ats-storable
exposed-modules: AesonDemo
hs-source-dirs: .
ghc-options: -Wall -Werror -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat

Edit the build-depends field so that it contains the following:

  build-depends: base < 5
, ats-storable
, aeson
, bytestring

The Haskell side of things will contain most of the boilerplate. This is unfortunate, but for simple cases it's manageable. For the sake of our tutorial, the example will be fairly simple. JSON only has product types, so all the heavy lifting can be done by generics.

Open up hs/AesonDemo.hs; you should see the following:

{-# LANGUAGE ForeignFunctionInterface #-}module AesonDemo wherehello_world :: IO ()
hello_world = putStrLn "Hello from Haskell!"foreign export ccall hello_world :: IO ()

We'll change the templated file to the following:

{-# LANGUAGE DeriveAnyClass           #-}
{-# LANGUAGE DeriveGeneric            #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE IncoherentInstances      #-}module AesonDemo whereimport           Control.Monad
import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BSL
import           Data.Maybe                 (fromJust)
import           Foreign.C.String
import           Foreign.Ptr
import           Foreign.Storable.ATS
import           GHC.Genericsdata Person a b = Person {
name :: a
, age  :: b
} deriving (Generic, ToJSON, FromJSON, ATSStorable)type YoungEnglishSpeakingPerson = (Person CString Int)decodeFail :: String -> Person String Int
decodeFail = fromJust . decode . BSL.packstrToCStr :: Person String a -> IO (Person CString a)
strToCStr (Person s x) = Person <$> newCString s <*> pure xdecode_json :: CString -> IO (Ptr YoungEnglishSpeakingPerson) decode_json cstr = g =<< peekCString cstr where g = writePtr <=< strToCStr . decodeFailforeign export ccall decode_json :: CString -> IO (Ptr YoungEnglishSpeakingPerson) This is all a bit dense. Let's start by examining the extensions. {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE IncoherentInstances #-} Most of this is pretty familiar - ForeignFunctionInterface to allow us to export Haskell functions, and various Derive extensions allow us to derive ATSStorable for YoungEnglishSpeakingPerson. The presence of IncoherentInstances may be unsettling however. There's not really a sufficient reason behind this, other than that the author of ats-storable used some inadvisable instances to get things working. It will suit our case just fine. data Person a b = Person { name :: a , age :: b } deriving (Generic, ToJSON, FromJSON, ATSStorable)type YoungEnglishSpeakingPerson = (Person CString Int) Next are our data types. We create one (general) type that will suit both marshaling to foreign values and decoding from JSON. Note the use of CString for our second type - this will enable sharing of strings between Haskell and ATS. decodeFail :: String -> Person String Int decodeFail = fromJust . decode . BSL.packstrToCStr :: Person String a -> IO (Person CString a) strToCStr (Person s x) = Person <$> newCString s <*> pure xdecode_json :: CString -> IO (Ptr YoungEnglishSpeakingPerson)
decode_json cstr = g =<< peekCString cstr
where g = writePtr <=< strToCStr . decodeFailforeign export ccall decode_json :: CString -> IO (Ptr YoungEnglishSpeakingPerson)

Finally, we get a small bit of boilerplate. Some of this is predictable (a foreign export declaration), but there are also a few regrettable conversions between string types.

Note that ATSStorable provides the writePtr function - this allows us to directly write a value to some pointer, which will then be passed to ATS.

# ATS

Open up src/aeson-demo.dats. You should see the following:

%{^
#define STUB_H "hs/AesonDemo_stub.h"
#define STG_INIT __stginit_AesonDemo
%}#include "$PATSHOMELOCS/hs-bind-0.4.1/runtime.dats"extern fun hs_hello_world() : void = "mac#hello_world"implement main0(argc, argv) = { val _ = hs_init(argc, argv) val _ = hs_hello_world() val _ = hs_exit() } The ATS will have been (partly) generated by atspkg for us; we will replace the ATS side of things with the following: %{^ #define STUB_H "hs/AesonDemo_stub.h" #define STG_INIT __stginit_AesonDemo %}#include "share/atspre_staload.hats" #include "$PATSHOMELOCS/hs-bind-0.4.1/runtime.dats"staload UN = "prelude/SATS/unsafe.sats"
staload ".atspkg/hs2ats/gen.sats"fun free_yesp(x: young_english_speaking_person) : void =
strptr_free(x.name)fun print_p(j : !young_english_speaking_person) : void =
{
val _ = println!(j.name)
val _ = println!(j.age)
}extern
fun hs_decode_json(string) : ptr =
"mac#decode_json"implement main0 (argc, argv) =
{
val _ = hs_init(argc, argv)
val p = hs_decode_json("{ \"name\": \"Joe\", \"age\": 12 }")
val _ = hs_exit()
val j = $UN.ptr0_get(p) val _ = print_p(j) val _ = free_yesp(j) } This is also a bit dense. Let's begin by examining the spliced C and corresponding includes. %{^ #define STUB_H "hs/AesonDemo_stub.h" #define STG_INIT __stginit_AesonDemo#include "share/atspre_staload.hats" #include "$PATSHOMELOCS/hs-bind-0.4.1/runtime.dats"
%}

This defines two C macros that are used by the hs_bind package to initialize the Haskell runtime. We then have a bit of ATS boilerplate:

fun free_yesp(x : young_english_speaking_person) : void =
strptr_free(x.name)fun print_p(j : !young_english_speaking_person) : void =
{
val _ = println!(j.name)
val _ = println!(j.age)
}

The free_yesp function is necessary as atspkg generates only viewtypes by default and hence converted out Haskell String to ATS' Strptr1 (this is necessary for memory-safe programs as running two garbage collectors at once is inadvisable). You can examine .atspkg/hs2ats/gen.sats if you want to know exactly what the type definitions are.

Next is the external function declaration (we exported this function in the Haskell module earlier):

extern
fun hs_decode_json(string) : ptr =
"mac#decode_json"

Finally, the main0 function:

implement main0 (argc, argv) =
{
val _ = hs_init(argc, argv)
val p = hs_decode_json("{ \"name\": \"Joe\", \"age\": 12 }")
val _ = hs_exit()
val j = \$UN.ptr0_get(p)
val _ = print_p(j)
val _ = free_yesp(j)
}

As Haskell functions require the Haskell runtime to work at all, we call hs_init() before doing anything. We only keep the Haskell runtime active long enough to call hs_decode_json(), after which we can call hs_exit(), read the value at the pointer, and print it out as in a normal ATS program.

We can now build the project with

atspkg build

If all goes well, you can the run the demo with

./target/aeson-demo

# Coda

This is not the same as actual library support. But crucially, it's easier than writing an entire library in ATS; one of the strengths of ATS is its ability to interface with foreign code written in a language that has features that it doesn't (in this case, generic data types). As writing ATS can be difficult at times, this is a welcome technique.