I wanted to give a small demonstration of ATS, partly to document a little bit of how to use atspkg, partly to show how to (safely) beat C's performance in ATS, and partly an example of how to parse command-line arguments in ATS.

We won't do a full implementation of wc here, but hopefully this post will give an indication of how one would do such a thing.

Installing Tooling

If you are on Linux or Mac, you can install atspkg with

curl -sSl https://raw.githubusercontent.com/vmchale/atspkg/master/bash/install.sh | bash -s

Otherwise, if you are a cabal user, you can install with

cabal new-install ats-pkg --symlink-bindir ~/.local/bin --happy-options='-gcsa' --alex-options='-g'

Next, you'll want to install pi, a project templating tool that has support for ATS. You can do this with

curl -LSfs https://japaric.github.io/trust/install.sh | sh -s -- --git vmchale/project-init

If you'd like to compile from source (you will have to install cargo):

cargo install project_init

Writing Code

Configuration

First, let's create a new project:

pi new ats ats-wc cd ats-wc

We'll need to add linecount as a dependency. This is just a packaged version of a code example included in the examples; in general such a package will not exist, but there are many beautiful bits of code scattered about and you can collect them with relatively little effort.

Open up atspkg.dhall. You should see this:

let prelude = https://raw.githubusercontent.com/vmchale/atspkg/master/ats-pkg/dhall/atspkg-prelude.dhall

in prelude.default // { bin = [ prelude.bin // { src = "src/ats-wc.dats" , target = "target/ats-wc" } ] }

We'll add a field for our dependency to the outermost record:

let prelude = https://raw.githubusercontent.com/vmchale/atspkg/master/ats-pkg/dhall/atspkg-prelude.dhall

in prelude.default // { bin = [ prelude.bin // { src = "src/ats-wc.dats" , target = "target/ats-wc" } ] , dependencies = [ "ats-linecount" ] }

Great! Now we're ready to begin in earnest.

ATS

Open up src/ats-wc.dats. You should see this:

implement main0 () = println!("Hello, World!")

Since linecount already provides us with the function

fun line_count(s : string) : int

we simply need to provide a list of files and handle command-line arguments.

This turns out to be fairly involved in ATS. ATS provides two values to help us out here - argc (the number of arguments) and argv (their values as strings).

Our first step is to determine if a user passed the -h or --help flag. We also filter out any unrecognized command-line flags in this step.

fn should_help { n : nat | n > 0 }(argc : int(n), argv : !argv(n)) : bool = let fun loop {n:nat}{ m : nat | m < n } .<n-m>. (i : int(m), argc : int(n), argv : !argv(n)) : bool = let var s = argv[i] in ifcase | s = "-h" || s = "--help" => true | is_flag(s) => (prerr!("\33[31mError:\33[0m flag not recognized: '" + s + "'\n") ; exit(1)) | i + 1 < argc => loop(i + 1, argc, argv) | _ => false end in loop(0, argc, argv) end

There's a lot going on here. The first thing to notice is the use of refinement types such as

{ n : nat | n > 0 }(argc : int(n), argv : !argv(n))

The syntax is a bit odd, but this is just a universal quantifier as a type. Crucially, ATS allows us to quantify over both int(n) and !argv(n) at the same time! This constraint means that argv (the array containing the values) has n elements, and also that argc is equal to n. The next bit is a little more exciting:

{n:nat}{ m : nat | m < n } .<n-m>. (i : int(m), argc : int(n), argv: !argv(n))

Here, we have two bound variables, m and n, with the constraint that m is less than n. This also means that i will be less than the number of elements in argv - crucially, this implies that argv[i] is safe.

The termination metric .<n-m>. is notable as well. Unlike Idris, ATS does not have a totality checker. Instead, we prove a function terminates by showing that some value decreases on every function call - in this case, n-m and hence argc - i.

The rest of the function is fairly typical ATS. We introduce a locally-scoped function loop and call it on some an initial index, ensuring safety with type-based static checks.

We can write a bare-bones help output as follows:

fun display_help() : void = println!("ats-wc - line counter written in ATS \33[36mUSAGE:\33[0m ats-wc [FLAG] ... [FILE] ... \33[36mFLAGS:\33[0m -h, --help Display this help")

And then put this all together:

implement main0 (argc, argv) = if should_help(argc, argv) then display_help()

At this point, you can run

atspkg build ./target/ats-wc --help

and help information will be displayed.

The next bit we have to work out is how to parse rest of the command-line arguments. This doesn't involve any new concepts, but it is quite long.

fn cli_loop { n : nat | n > 1 }(argc : int(n), argv : !argv(n)) : void = let fn p_wc {n:nat}{ m : nat | m < n }(i : int(m), argv : !argv(n)) : int = let var s = argv[i] in let var c = line_count(s) val _ = if test_file_isdir(s) = 0 then println!(c, " ", s) in c end end fun loop {n:nat}{ m : nat | m < n } .. (i : int(m), argc : int(n), argv : !argv(n)) : int = ifcase | i + 1 < argc => let var total = p_wc(i, argv) in total + loop(i + 1, argc, argv) end | _ => p_wc(i, argv)

var total = loop(1, argc, argv)

in if argc >= 3 then println!(total, " total") end

The main purpose of this function is simply printing out line number information for each file passed in as an argument, as well as a total if there are multiple files. The above function also filters out directories, so that they are not included in the output.

We can tie this all together by modifying main0 to:

implement main0 (argc, argv) = if should_help(argc, argv) then display_help() else if argc > 1 then cli_loop(argc, argv) else (prerr!("\33[31mError:\33[0m no input given\n") ; exit(1))

If all went well, you src/ats-wc.dats should look something like this:

#include "$PATSHOMELOCS/ats-linecount-0.2.2/lines.dats" #include "share/atspre_staload_libats_ML.hats"

staload "libats/ML/SATS/string.sats"

fun display_help() : void = println!("ats-wc - line counter written in ATS \33[36mUSAGE:\33[0m ats-wc [FLAG] ... [FILE] ... \33[36mFLAGS:\33[0m -h, --help Display this help")

fun is_flag(s : string) : bool = string_is_prefix("-", s)

fn should_help { n : nat | n > 0 }(argc : int(n), argv : !argv(n)) : bool = let fun loop {n:nat}{ m : nat | m < n } .<n-m>. (i : int(m), argc : int(n), argv : !argv(n)) : bool = let var s = argv[i] in ifcase | s = "-h" || s = "--help" => true | is_flag(s) => (prerr!("\33[31mError:\33[0m flag not recognized: '" + s + "'\n") ; exit(1)) | i + 1 < argc => loop(i + 1, argc, argv) | _ => false end in loop(0, argc, argv) end

fn cli_loop { n : nat | n > 1 }(argc : int(n), argv : !argv(n)) : void = let fn p_wc {n:nat}{ m : nat | m < n }(i : int(m), argv : !argv(n)) : int = let var s = argv[i] in let var c = line_count(s) val _ = if test_file_isdir(s) = 0 then println!(c, " ", s) in c end end fun loop {n:nat}{ m : nat | m < n } .<n-m>. (i : int(m), argc : int(n), argv : !argv(n)) : int = ifcase | i + 1 < argc => let var total = p_wc(i, argv) in total + loop(i + 1, argc, argv) end | _ => p_wc(i, argv) var total = loop(1, argc, argv) in if argc >= 3 then println!(total, " total") end

implement main0 (argc, argv) = if should_help(argc, argv) then display_help() else if argc > 1 then cli_loop(argc, argv) else (prerr!("\33[31mError:\33[0m no input given\n") ; exit(1))

You can now build and run with

atspkg build ./target/ats-wc src/ats-wc.dats

Benchmarks

This is actually faster than wc. You can find more details here. I ran this benchmark on the full text of Ulysses.

Tool Language Time
ats-wc ATS 1.362 ms
wc C 1.513 ms