OxL Reference

Text and the command line

Word frequencies#

Task. Count how often each word occurs and print the most frequent ones.

oxl✓ checked in CI
const TEXT = "the quick brown fox jumps over the lazy dog the fox"

fn countWords(text: str) -> Map<str, u64>  pure {
  var counts: Map<str, u64> = {:}
  for w in text.splitWhitespace() {
    counts[w.lower()] = (counts.get(w.lower()) ?? 0) + 1  // get returns u64?, ?? 0 when the word has not been seen
  }
  counts
}

fn main() io {
  let counts = countWords(TEXT)
  let top = from (w, n) in counts order by n desc, w take 3 select "{w}: {n}"  // top 3: by descending count, then alphabetically
  for line in top { print(line) }
  print("distinct words: {counts.len()}")
}
Output
the: 3
fox: 2
brown: 1
distinct words: 8
  • counts[w.lower()] = (counts.get(w.lower()) ?? 0) + 1 is the classic counter: get returns u64?, and ?? 0 provides the initial value.
  • The query from (w, n) in counts order by n desc, w take 3 select … sorts the pairs of the map and takes the first three. The second sort key (w) makes the order deterministic for equal counts.
  • Pitfall: a Map iterates in insertion order, not alphabetically - sort explicitly if you need alphabetical order.

Parsing CSV with errors#

Task. Read tabular data, find bad rows and aggregate the good ones.

oxl✓ checked in CI
data Sale { region: str; product: str; qty: i64; price: i64 }

@error data BadRow { line: usize; why: str }

const CSV = "region,product,qty,price\nnorth,pen,10,3\nsouth,ink,2,15\nnorth,ink,1,15\nsouth,pen,7,3"

fn parseRow(n: usize, line: str) -> Sale | BadRow {
  let cols = line.split(",")
  if cols.len() != 4 { fail BadRow{ line: n, why: "expected 4 columns, got {cols.len()}" } }
  let qty = match cols[2].trim().parseInt() {
    i64 v => v
    ParseError => fail BadRow{ line: n, why: "qty is not a number" }
  }
  let price = match cols[3].trim().parseInt() {
    i64 v => v
    ParseError => fail BadRow{ line: n, why: "price is not a number" }
  }
  Sale{ region: cols[0], product: cols[1], qty, price }
}

fn parseAll(text: str) -> List<Sale> | BadRow {
  var rows: List<Sale> = []
  for (i, line) in text.lines().iter().enumerate().skip(1) {  // skip the header
    rows.push(parseRow(i + 1, line)?)
  }
  rows
}

fn main() io {
  match parseAll(CSV) {
    List<Sale> rows => {
      let byRegion = from s in rows group by s.region into g select (g.key, g.count())
      for (region, n) in byRegion { print("{region}: {n} sales") }
      let revenue = rows.iter().map(s => s.qty * s.price).sum()
      print("revenue: {revenue}")
    }
    BadRow e => print("line {e.line}: {e.why}")
  }
  match parseAll("h\nnorth,pen,x,3") {
    List<Sale> rows => print("{rows.len()} rows")
    BadRow e => print("line {e.line}: {e.why}")
  }
}
Output
north: 2 sales
south: 2 sales
revenue: 96
line 2: qty is not a number
  • parseRow returns Sale | BadRow: the error knows the line number and the reason. fail BadRow{ … } is an early exit.
  • match s.parseInt() takes i64 | ParseError apart; we convert the error into our own, more meaningful one.
  • rows.push(parseRow(i + 1, line)?) - ? stops parsing at the first bad row. To collect all errors, accumulate them in a List<BadRow> instead of using ?.
  • from s in rows group by s.region into g select (g.key, g.count()) is a grouping; rows.iter().map(…).sum() is an aggregate.
  • For real CSV with quotes use a format library; split(",") is fine for simple data.

Command-line arguments#

Task. Parse --name Ann --times 3 --loud without external libraries.

oxl✓ checked in CI
use std.os { args }

@error data Usage { msg: str }

data Options { name: str; times: i64; loud: bool }

fn parseArgs(argv: List<str>) -> Options | Usage {
  var opts = Options{ name: "world", times: 1, loud: false }
  var i = 0
  while i < argv.len() {
    match argv[i] {
      "--name" => {
        opts.name = argv.get(i + 1) ?? fail Usage{ msg: "--name needs a value" }
        i += 1
      }
      "--times" => {
        let raw = argv.get(i + 1) ?? fail Usage{ msg: "--times needs a value" }
        match raw.parseInt() {
          i64 n => opts.times = n
          ParseError => fail Usage{ msg: "--times: not a number: {raw}" }
        }
        i += 1
      }
      "--loud" => opts.loud = true
      other => fail Usage{ msg: "unknown option {other}" }
    }
    i += 1
  }
  opts
}

fn main() -> () | Usage  io {
  let opts = parseArgs(args().iter().skip(1).toList())?  // args()[0] is the program name
  for _ in 0..opts.times {
    let text = "Hello, {opts.name}!"
    print(if opts.loud { text.upper() } else { text })
  }
  print("{parseArgs(["--times", "2", "--loud", "--name", "Ann"]) is Options}")
  match parseArgs(["--bogus"]) {
    Options _ => print("ok")
    Usage e => print("usage error: {e.msg}")
  }
}
Output
Hello, world!
true
usage error: unknown option --bogus
  • std.os.args() returns List<str>; element zero is the program name, hence skip(1). Run: oxl run prog.oxl -- --name Ann --loud.
  • Parsing is a pure function parseArgs(argv): it is easy to test without launching the program (in the example it is called with ready-made lists).
  • argv.get(i + 1) ?? fail Usage{…} is "a value or a usage error": fail is an expression of type never, so it can stand to the right of ??.
  • fn main() -> () | Usage io - an unhandled Usage ends the process with exit code 1 and a message.

An aligned table#

oxl✓ checked in CI
fn row(cols: List<str>, widths: &List<usize>) -> str  pure {
  var parts: List<str> = []
  for (i, c) in cols.iter().enumerate() {
    parts.push(c.padEnd(widths[i], ' '))  // padEnd/padStart align to a width
  }
  parts.iter().fold("", (a, p) => if a.isEmpty() { p } else { "{a} | {p}" })
}

fn main() io {
  let items = [("pen", 3, 10), ("notebook", 120, 2), ("ink", 15, 1)]
  let widths: List<usize> = [10, 6, 5]
  print(row(["item", "price", "qty"], &widths))
  print("-".repeat(25))
  var total = 0
  for (name, price, qty) in items {
    print(row([name, "{price}".padStart(5, ' '), "{qty}".padStart(5, ' ')], &widths))
    total += price * qty
  }
  print("total: {total}")
}
Output
item       | price  | qty
-------------------------
pen        |     3  |    10
notebook   |   120  |     2
ink        |    15  |     1
total: 285

padEnd and padStart pad a string to a width; "-".repeat(25) draws a line. The column widths are passed by borrow (&widths): the list stays with the caller and is used many times (without & the second call would give use-after-move).

Files#

Task. Write a file, append to it, read it, look at the metadata and the directory, and handle a missing file gracefully.

oxl✓ checked in CI
use std.io { File, fs }

fn main() -> () | IoError  io {
  let dir = fs.tempDir()?  // a temp directory, removed at the end
  let path = "{dir}/notes.txt"
  File.writeText(path, "first\nsecond\n")?
  File.appendText(path, "third\n")?
  let text = File.readText(path)?
  let lines = text.lines()
  print("{lines.len()} lines, last: {lines.last() ?? "-"}")
  let meta = File.metadata(path)?
  print("size {meta.size} bytes, is file: {meta.isFile()}")
  File.writeText("{dir}/other.txt", "x")?
  let entries = fs.list(dir)?
  let names = entries.iter().map(e => e.name).sortBy(n => n).toList()
  print("{names}")
  match File.readText("{dir}/missing.txt") {  // an I/O error is a value
    str _ => print("unexpected")
    IoError e => print("cannot read: {e.kind}")
  }
  fs.removeAll(dir)?
  print("cleaned: {!File.exists(path)}")
}
Output
3 lines, last: third
size 19 bytes, is file: true
["notes.txt", "other.txt"]
cannot read: NotFound
cleaned: true
  • All std.io.File operations return … | IoError and need the io effect. fn main() -> () | IoError io lets you write ? everywhere.
  • fs.tempDir() creates a temporary directory - safe for examples and tests; fs.removeAll deletes it.
  • IoError is a value with a kind field (NotFound, PermissionDenied, …); this lets you react differently: create the file if it is missing but stop on a permission error.
  • For large files read in parts with streams (std.stream) rather than the whole readText.

Back: cookbook.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides