by Vanessa McHale | 2017-12-13 18:13

In this tutorial, we reimplement several of wc 's features in ATS. The core logic (as we shall see) is relatively simple, and we end up with something that's competitive with C with relatively little effort.

First, we will handle the easier bit: counting. ATS provides several functions to stream from a file, namely streamize_fileref_line , streamize_fileref_word , and streamize_fileref_char . Do note that we will use the versions from libats/ML rather than prelude . For some reason there are two different functions named streamize_fileref_line in the standard library, so one must be careful.

Let's get our imports out of the way (they're not particularly interesting):

#include "share/atspre_define.hats" #include "share/atspre_staload.hats" #include "prelude/DATS/filebas.dats" #include "libats/ML/DATS/filebas_dirent.dats" #include "libats/libc/DATS/dirent.dats" staload "libats/ML/DATS/string.dats" staload "libats/ML/DATS/filebas.dats" staload EXTRA = "libats/ML/SATS/filebas.sats"

ATS is a functional programming language, so we will use higher-order functions to simplify the problem in question.

We'll first define count to count streams generated from a file. It will have the following type signature:

fun {a:t@ype}count(s: string, fcount: FILEref -<cloptr1> stream_vt(a)) : int

This takes a function fcount which takes FILEref as an argument and returns a stream_vt(a) . For now, you can think of -<cloptr1> as somewhat like -> in Haskell or Idris.

With the type signature in mind, we can express the counting functions like so:

fun line_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_line(x)) - 1 fun word_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_word(x)) - 1 fun char_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_char(x))

This is exactly what we want - we pass in a string containing the filepath, and we get back the relevant information. Now we can return to count , giving it the following implementation:

fun {a:t@ype}count(s: string, fcount: FILEref -<cloptr1> stream_vt(a)) : int = let var ref = fileref_open_opt(s, file_mode_r) val x = case ref of | ~Some_vt(x) => begin let var viewstream = fcount(x) var n: int = stream_vt_length(viewstream) val _ = fileref_close(x) in n end end | ~None_vt() => (println!("could not open file at " + s) ; exit(1) ; 0) val _ = cloptr_free($UN.castvwtp0(fcount)) in x end

There is quite a lot going on here. First, we note that fcount has linear type - we have to free fcount or the compiler will give us a type error. So it turns out -<cloptr1> is not quite the same as -> .

Next, note the use of stream_vt_length . Since stream_vt is a linear type, it must be fully consumed. Indeed, if we had written

var n: int = 0

instead, our program would not typecheck. Thankfully, consuming the value is relatively easy in this example.

The series of motions we go through to open a file should be familiar enough.

We are now ready to handle parsing command-line arguments. Unfortunately (and unsurprisingly), there is no library to do this. The approach will generalize, but it will not be pleasant.

Our first step is to set out some data types for the command-line parser. We'll go for a simple approach, using sum types and record types that will be familiar to anyone with a familiarity with functional programming.

datatype counter = | lines | words | chars | unknown typedef command_line = @{ file_name = Option(string) , to_count = counter }

Our wc clone will be pretty limited in functionality; in particular it will only print one count at a time. We will parse an Option(string) to hold the file name, failing later if none is present.

Next we write some helper functions to modify a command_line record.

fun set_target(acc: command_line, s: string) : command_line = let val b = case+ acc.file_name of | None _ => true | _ => false val acc_r = ref<command_line>(acc) val _ = if b then acc_r->file_name := Some(s) in !acc_r end

This function will take a string, the previous state of command_line , and it will return another command_line . It also prevents us from reading more than one file at once (unlike the real wc ). We can do something similar for handling the actual flags:

fun set_lines(acc: command_line, ct: counter) : command_line = let val acc_r = ref<command_line>(acc) val _ = acc_r->to_count := ct in !acc_r end

With that in place, we can write the actual parser. We don't bother handling various edge cases in this example (e.g. when the user specifies both -l and -w ) as it is tedious and not particularly illuminating.

fun process(arg: string, acc: command_line) : command_line = case+ arg of | "-l" => set_lines(acc, lines) | "-w" => set_lines(acc, words) | "-c" => set_lines(acc, chars) | s => set_target(acc, s) fnx get_cli { n : int | n >= 1 } { m : nat | m < n } .<n-m>. ( argc: int n , argv: !argv(n) , current: int m , acc: command_line ) : command_line = let var arg = argv[current] in if current < argc - 1 then let val acc_next = get_cli(argc, argv, current + 1, acc) in process(arg, acc_next) end else process(arg, acc) end

We've split out process for readability's sake, but everything else here is fairly standard. We use refinement types (essentially universal quantifiers) in get_cli to check at compile time that all array accesses are safe. Note also the use of .<n-m>. here: the termination metric proves that our parser will terminate.

At this point we're nearly done; we need a few trivial bits of control flow.

fun get_file(f: Option(string)) : string = case+ f of | Some(x) => x | None => (prerr!("Need to specify a filename.

") ; exit(1)) implement main0 (argc, argv) = let val cli = @{ file_name = None , to_count = unknown } val _ = if argc = 1 then (prerr!("Need to specify a filename.

") ; exit(1)) val parsed = get_cli(argc, argv, 0, cli) val file_name = get_file(parsed.file_name) in case+ parsed.to_count of | lines _ => println!(tostring_int(line_count(file_name)) + " " + file_name) | words _ => println!(tostring_int(word_count(file_name)) + " " + file_name) | chars _ => println!(tostring_int(char_count(file_name)) + " " + file_name) | unknown _ => println!("Please provide one of '-l', '-w', or '-c'") end

Putting this all together, you should have something like the following:

#include "share/atspre_define.hats" #include "share/atspre_staload.hats" #include "prelude/DATS/filebas.dats" #include "libats/ML/DATS/filebas_dirent.dats" #include "libats/libc/DATS/dirent.dats" staload "libats/ML/DATS/string.dats" staload "libats/ML/DATS/filebas.dats" staload EXTRA = "libats/ML/SATS/filebas.sats" datatype counter = | lines | words | chars | unknown typedef command_line = @{ file_name = Option(string) , to_count = counter } fun {a:t@ype}count(s: string, fcount: FILEref -<cloptr1> stream_vt(a)) : int = let var ref = fileref_open_opt(s, file_mode_r) val x = case ref of | ~Some_vt(x) => begin let var viewstream = fcount(x) var n: int = stream_vt_length(viewstream) val _ = fileref_close(x) in n end end | ~None_vt() => (println!("could not open file at " + s) ; exit(1) ; 0) val _ = cloptr_free($UN.castvwtp0(fcount)) in x end fun line_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_line(x)) - 1 fun word_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_word(x)) - 1 fun char_count(s: string): int = count(s, lam x => $EXTRA.streamize_fileref_char(x)) fun set_lines(acc: command_line, ct: counter) : command_line = let val acc_r = ref<command_line>(acc) val _ = acc_r->to_count := ct in !acc_r end fun set_target(acc: command_line, s: string) : command_line = let val b = case+ acc.file_name of | None _ => true | _ => false val acc_r = ref<command_line>(acc) val _ = if b then acc_r->file_name := Some(s) in !acc_r end fun process(arg: string, acc: command_line) : command_line = case+ arg of | "-l" => set_lines(acc, lines) | "-w" => set_lines(acc, words) | "-c" => set_lines(acc, chars) | s => set_target(acc, s) fnx get_cli { n : int | n >= 1 } { m : nat | m < n } .<n-m>. ( argc: int n , argv: !argv(n) , current: int m , acc: command_line ) : command_line = let var arg = argv[current] in if current < argc - 1 then let val acc_next = get_cli(argc, argv, current + 1, acc) in process(arg, acc_next) end else process(arg, acc) end fun get_file(f: Option(string)) : string = case+ f of | Some(x) => x | None => (prerr!("Need to specify a filename.

") ; exit(1)) implement main0 (argc, argv) = let val cli = @{ file_name = None , to_count = unknown } val _ = if argc = 1 then (prerr!("Need to specify a filename.

") ; exit(1)) val parsed = get_cli(argc, argv, 0, cli) val file_name = get_file(parsed.file_name) in case+ parsed.to_count of | lines _ => println!(tostring_int(line_count(file_name)) + " " + file_name) | words _ => println!(tostring_int(word_count(file_name)) + " " + file_name) | chars _ => println!(tostring_int(char_count(file_name)) + " " + file_name) | unknown _ => println!("Please provide one of '-l', '-w', or '-c'") end

You can compile this with

$ patscc wc.dats -DATS_MEMALLOC_LIBC -o ats-wc -cleanaft -O2 -mtune=native

And run it with

$ ./ats-wc wc.dats -l 117 src/wc.dats

And that's it! We've written a fast, functional implementation of a common tool in 117 lines. It's certainly not complete, but I hope this gives some insight into writing useful programs in ATS.