Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Collections and Pipelines

Collections are values. Every method returns a new collection and leaves the old one alone, and the native compiler reuses memory in place when nothing else can see the old value, and frees a list or string at its last use when it has one owner, so this costs less than it sounds.

Pipelines

Most list code is a chain of methods, each taking a lambda:

type Order = {customer: Str, item: Str, cents: Int where _ >= 0}

fn top_customers(orders: List[Order], n: Int) -> List[(Str, Int)]
= do
  totals = orders.group_by(_.customer).map(g => (g.0, g.1.map(_.cents).sum))
  totals.sort_by((-_.1, _.0)).take(n)

group_by gives (key, items) pairs in first-seen order. sort_by takes a key; a tuple key sorts by several fields, and negating a number sorts it descending. A chain can continue on the next line when that line is indented deeper and starts with .name, as in = orders followed by .map(_.cents) and .sum on lines of their own; binding the middle step to a name, as above, works too.

$ sspur run collections.ssp
ana: 1000
bo: 900
$ sspur test collections.ssp
4 passed, 0 failed

Lists

test list_ops = do
  xs = [5, 3, 8, 3]
  xs.filter(_ > 4) == [5, 8] and xs.unique == [5, 3, 8] and xs.fold(0, (a, x) => a + x) == 19 and xs.enumerate.first == some((0, 5)) and xs.windows(2).len == 3 and xs.partition(_ % 2 == 0) == ([8], [5, 3, 3])

The methods you will use most are map filter fold sum len sort sort_by find any all first last take drop contains unique enumerate zip join group_by counts partition windows chunks. Methods that may find nothing, like find, first and index_of, return an Opt. A List is also a deque: push, push_front, pop_front and pop_back are amortized O(1) in native code. The collections reference lists every method.

Maps and sets

Map and Set are ordered and persistent: put and add return a new map or set. HashMap and HashSet have the same methods with hashing, and print in key order.

test maps = do
  stock = empty_map().put("pen", 3).put("ink", 0)
  later = stock.put("pen", 2)
  stock.get("pen") == some(3) and later.get("pen") == some(2) and stock.keys == ["ink", "pen"] and stock.has("cap") == false

test sets = do
  a = [1, 2, 3].to_set
  b = [2, 3, 4].to_set
  a.inter(b).items == [2, 3] and a.union(b).len == 4 and a.diff(b).has(1)

Loops and ranges

for works over lists, ranges and generators, and a for pattern can take a tuple apart. a..b excludes b; range(start, end, step) counts by any step, including down. str_buf() builds a string in amortized O(1) per append.

fn histogram(words: List[Str]) -> Str
= do
  var out = str_buf()
  for (w, n) in words.counts.sort_by(_.0)
    out := out.add("{w}={"*".repeat(n)} ")
  out.str.trim

test loops = histogram(["b", "a", "b"]) == "a=* b=**" and range(10, 0, -3) == [10, 7, 4, 1] and (0..5).map(_ * _).sum == 30

Loops over ranges, and map, filter and sum chains over them, run natively without building the list, and so does a map or filter followed by a fold whose function has no effects and captures nothing. Pure pipelines over large lists are also split across cores automatically, which Concurrency explains. Next: Concurrency.