by Vanessa McHale | 2017-12-02 07:23

ATS is an obscure language, and perhaps justifiably so. It was exactly as I was warned (insane, idiosyncratic syntax; key features barely documented; no tutorials), but it was also beautiful, moving, and surprisingly practical.

I stumbled upon ATS after a period of disillusionment with Rust. Having become quite proficient in Haskell, Rust struck me as verbose and inexpressive. This became inexcusable when I wrote a parser for hask-replace and struggled to get performance that beat Haskell. I wanted a functional, fast language for polyglot.

ATS turned out to be a surprisingly good choice for polyglot; since there is relatively little information on systems programming in ATS, I figured I'd write up two short examples drawn from "real-world" use so that others might be able to experiment with functional systems programming as well.

Directory Streaming

Directory streaming in ATS is easier than doing it in Rust, provided you get past some of ATS' idiosyncrasies - note the use of the fnx keyword to denote a recursive function and the and keyword for a mutually recursive function.

When you get past that, it starts to look nicer. We lazily stream a directory's contents (recursively!), adding one to the counter every time we encounter a file. We filter out ./ and ../ so that the recursion actually terminates. There is some subtlety here - streamize_dirname_fname returns a linear stream - but once written the code is familiar to anyone with a bit of knowledge of functional programming.

#include "share/atspre_staload.hats" #include "libats/ML/DATS/filebas_dirent.dats" #include "libats/libc/DATS/dirent.dats" staload "libats/ML/DATS/string.dats" fun bad_dir(next: string) : bool = case next of | "." => true | ".." => true | _ => false fnx step_stream(acc: int, s: string) : int = if test_file_isdir(s) = 1 then flow_stream(s, acc) else acc + 1 and flow_stream(s: string, init: int) : int = let var files = streamize_dirname_fname(s) // cloptr stands for "closure pointer", here written using a lambda var ffiles = stream_vt_filter_cloptr(files, lam x => not(bad_dir(x))) in stream_vt_foldleft_cloptr( ffiles , init , lam (acc, next) => step_stream(acc, s + "/" + next) ) end implement main0() = ()

You can compile this example with

$ patscc -DATS_MEMALLOC_LIBC stream.dats -o filecount -cleanaft

Unfortunately, finding these library functions was somewhat involved. I had to resort to ripgrep-ing the contents of /usr/local/lib/ats2-postiats-0.3.8/ in order to find streamize_dirname_fname and stream_vt_foldleft_cloptr , and then read the manual in order to understand how to use linear streams. Some of that was to be expected, as linear streams are a feature unique to ATS.

But we should still note that Rust doesn't really have any comparably nice way to accomplish this: recursion is (predictably) the best way to handle recursive directory descent.

String Padding

String padding is a bit trivial, but it is a nice example of how to use some of the advanced type system features that do not exist in any other systems programming language.

#include "share/atspre_define.hats" #include "share/atspre_staload.hats" #include "share/HATS/atslib_staload_libats_libc.hats" %{^ #include "libats/libc/CATS/string.cats" %} staload "libats/ML/DATS/string.dats" fnx right_pad { k: int | k >= 0 }{ m: int | m <= k } .<k>. (s: string(m), n: int(k)) : string = case+ length(s) < n of | true when n > 0 => right_pad(s, n - 1) + " " | _ => s implement main0() = ()

The first thing to note is the use of refinement types here. They are enclosed in curly braces { k: int | k >= 0 } and function as universal quantifiers. You can read it as "for all integers \(k\) less than or equal to \(0\)" (define a function right_pad ).

Next we note the use of dependent types. int(k) is a type for integers bounded by k . So { k: int | k >= 0 }(n : int(k)) means n is an integer that can be proven to be bounded at compile-time. Additionally, we can see that s must have length less than or equal to k - the function will never allow a string of length 22 to be padded to length 20, for example. This encodes something we know about the semantics of the function into the type system, preventing wasteful function calls.

Finally, we note something unique to ATS - the use of a termination metric, denoted .<k>. . This tells the compiler that k must strictly decrease on each function call; along with the condition imposed by { k : int | k >= 0 } this guarantees that the function is well-defined.

Coda

ATS is nowhere near as user-friendly as Rust, or even C. But it is nonetheless beautiful, vast, and in all probability the future of systems programming - just these short examples contain two features completely unique to ATS.