Skip to content

runtime library ​

Ghul.Runtime ships alongside the compiler and supplies Pipe[T] and other everyday building blocks used throughout this site. The reference below covers Ghul.Pipes, the sequence-processing library behind filter, map, reduce and the thread-first operator, and then how the runtime displays values.

A pipe combinator chain is written with the thread-first operator |> over free functions, which pass the sequence in as the first argument:

ghul
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let sum_of_even_squares = numbers
|> filter(x => x % 2 == 0)
|> map(x => x * x)
|> sum()
write_line("sum of even squares: {sum_of_even_squares}")
sum of even squares: 220

how a pipe runs ​

The combinators come in two kinds. A stage returns a new sequence, a T{}, which is what lets stages chain: map returns a sequence that maps, filter returns one that filters. A terminal returns something else - a value, a list, a count - so it is where a pipe ends.

Elements travel through a pipe one at a time, and the terminal is what pulls them through. It asks the pipe it was called on for an element, that pipe asks the one it was built from, and so on back to the iterable at the start; the element then makes its way down the pipe, each stage working on it before passing it on to the next stage. No stage buffers the whole sequence - typical stages hold only one element at a time - so a map over a million elements doesn't construct a million-element list.

Pipes are lazy: until something - a terminal - asks a pipe for elements, no stage runs. An inert pipe can be held or passed around until it's needed. And if the consumer stops pulling elements from the pipe, the pipe will stop pulling elements from its source iterator. Every read of a pipe starts from the beginning, pulling elements through its chain of stages from the source again, and two reads in progress at once are independent of each other.

ghul
let numbers = [1, 2, 3, 4, 5, 6]
// nothing has asked this pipe for elements yet, so peek's
// action has not run
let stages = numbers
|> peek(x => write_line(" pulled {x}"))
|> filter(x => x % 2 == 0)
|> map(x => x * 10)
write_line("pipe built - nothing has run yet")
// collect_mutable is a terminal, so it asks for the elements
let result = stages |> collect_mutable()
write_line("result: {result |> join(", ")}")

Because pipes are lazy, a source can have an infinite number of elements, such as a generator that yields indefinitely. Something downstream decides when to stop reading it: take(n) stops pulling after n elements have passed through it, and a terminal such as find stops at the first match.

reverse, the sort family, transpose and permutations do buffer: they need the whole sequence before they can produce anything, so they read the whole source first. They are listed separately below.

A source that holds a resource has to be disposed: the lines of a file, a directory listing, a database reader. Take its iterator with use, and build the pipe over that iterator with cursor. Here open_lines stands in for IO.File.read_lines(path).iterator, and prints a line when it is disposed:

ghul
first_long_line() -> string? =>
cursor(use open_lines()) |> find(line => line.length > 4)
write_line(first_long_line() ?? "none")
closing the lines
longer

cursor reads the iterator it is given, rather than asking the source for a new one. use disposes that iterator when the function returns, before its result is printed. Without use the lines would stay open: find stops at the first long line, and a terminal never disposes the iterator it reads.

The compiler reports an undisposed-source warning where a read of one of these sources can stop early with nothing to dispose it.

reading the signatures ​

The pure on a function type - predicate: (T) -> bool pure - asks that the function you pass only reads, and doesn't write to the heap. Most anonymous functions satisfy it without any thought; see type narrowing for what the compiler does with the guarantee.

A combinator that might not find anything returns T?, an optional type: it holds a T or doesn't hold one, and ??, ! and if let read the value out.

making a pipe ​

pipe ​

Turns any Iterable[T] - an array, a LIST[T], a MAP[K, V]'s values, anything with an .iterator - into a Pipe[T]. A chain rarely needs it: the free functions all take an Iterable[T], so a chain can start from the source itself.

ghul
◆pipe[T](source: Iterable[T]) -> Pipe[T] pure

stages ​

A stage returns a new sequence, so stages chain onto one another.

filter ​

ghul
◆filter[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> Iterable[T] pure

map ​

ghul
◆map[T..,U](
source: Iterable[T],
mapper: T.. -> U pure
) -> Iterable[U] pure

flat_map ​

Maps each element to an iterable and runs the results together into one sequence.

ghul
◆flat_map[T..,U](
source: Iterable[T],
mapper: T.. -> Iterable[U] pure
) -> Iterable[U] pure

skip ​

ghul
◆skip[T](source: Iterable[T], count: int) -> Iterable[T] pure

take ​

ghul
◆take[T](source: Iterable[T], count: int) -> Iterable[T] pure

skip_while ​

ghul
◆skip_while[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> Iterable[T] pure

take_while ​

ghul
◆take_while[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> Iterable[T] pure

The four set operations that follow all discard duplicates. This is what they do to the same pair of sources:

ghul
let left = [1, 2, 2, 3, 4]
let right = [3, 4, 5]
// all four remove duplicates, keeping the first occurrence
// of each element
write_line("distinct: {left |> distinct()}")
write_line("union_with: {left |> union_with(right)}")
write_line("intersect_with: {left |> intersect_with(right)}")
write_line("except: {left |> except(right)}")

distinct ​

Removes duplicates, keeping the first occurrence of each element. distinct, union_with, intersect_with and except all do this, so each produces a sequence with no repeats, in the order first seen. Elements are compared with =~ and get_hash_code, so a type used with these needs both.

ghul
◆distinct[T](source: Iterable[T]) -> Iterable[T] pure

union_with ​

Every element of both sources with duplicates removed, taking the left source's elements first.

ghul
◆union_with[T](
source: Iterable[T],
right: Iterable[T]
) -> Iterable[T] pure

intersect_with ​

Elements the left and right sources have in common, in the order the left source has them.

ghul
◆intersect_with[T](
source: Iterable[T],
right: Iterable[T]
) -> Iterable[T] pure

except ​

Elements of the left source that the right source doesn't have.

ghul
◆except[T](
source: Iterable[T],
right: Iterable[T]
) -> Iterable[T] pure

peek ​

Calls action on each element and passes it through unchanged.

ghul
◆peek[T..](source: Iterable[T], action: T.. -> void) -> Iterable[T] pure

chunk and windows both produce groups of elements, and differ in how the groups are cut:

ghul
let numbers = [1, 2, 3, 4, 5, 6, 7]
// chunk: the first three elements, then the next three, and so
// on. the last group is short when the source doesn't divide
// evenly
for group in numbers |> chunk(3) do
write_line("chunk: {group |> join(", ")}")
od
// windows: every run of three neighbouring elements, so each
// group shares two elements with the one before it. a group is
// always three long
for window in numbers |> windows(3) do
write_line("window: {window |> join(", ")}")
od

chunk ​

The first size elements, then the next size, and so on, each element appearing in one group only. The last group is short when the source doesn't divide evenly. Compare windows, below.

ghul
◆chunk[T](source: Iterable[T], size: int) -> Iterable[List[T]] pure

windows ​

Every run of size neighbouring elements: the first size, then the same run moved along by one, and so on. Each window therefore shares all but one of its elements with the window before it. A window is always size long, so a source with fewer than size elements produces none.

ghul
◆windows[T](
source: Iterable[T],
size: int
) -> Iterable[List[T]] pure

group ​

Runs of neighbouring equal elements, each a read-only list, compared with =~. A new run starts wherever an element differs from the one before it, so equal elements that are not neighbours go into separate runs. Compare group_by, below, which gathers every element with the same key wherever it appears.

ghul
let readings = [1, 1, 2, 3, 3, 3, 1]
// group: runs of neighbouring equal elements, so the final 1
// starts a run of its own
for span in readings |> group() do
write_line("run: {span |> join(", ")}")
od
ghul
◆group[T](source: Iterable[T]) -> Iterable[List[T]] pure

cat ​

Concatenation: every element of the left source, then every element of the right.

ghul
◆cat[T](source: Iterable[T], right: Iterable[T]) -> Iterable[T] pure

index ​

Pairs each element with its index. INDEXED_VALUE[T] has index and value, and destructures positionally, so for (i, x) in xs |> index() do reads the pair apart. The second form starts the index at a given number rather than at 0.

ghul
◆index[T](source: Iterable[T]) -> Iterable[INDEXED_VALUE[T]] pure
◆index[T](
source: Iterable[T],
index: int
) -> Iterable[INDEXED_VALUE[T]] pure

zip ​

Pairs elements of the source with elements of other, stopping when either side runs out. The second form combines each pair with a mapper instead of yielding a tuple.

ghul
◆zip[T,U](
source: Iterable[T],
other: Iterable[U]
) -> Iterable[(T,U)] pure
◆zip[T,U,TOut](
source: Iterable[T],
other: Iterable[U],
mapper: (T,U) -> TOut pure
) -> Iterable[TOut] pure

stages that buffer ​

These return a sequence, like any other stage, but they cannot work out their first element without having seen the last one. So they buffer the whole source before producing anything, rather than passing elements along one at a time. reverse and the sort family read the source the moment they are called, and transpose and permutations read it each time their result is read.

reverse ​

Yields the source's elements last to first.

ghul
◆reverse[T](source: Iterable[T]) -> Iterable[T] pure

sort ​

Yields the source's elements in order. The first form uses the element type's own ordering: sorting without a comparer needs an element type that defines <>, or is comparable on the .NET side. The other two forms take an IComparer[T] or a comparison function returning negative, zero or positive.

ghul
◆sort[T: Ghul.Comparable[T]](source: Iterable[T]) -> Iterable[T] pure
◆sort[T](
source: Iterable[T],
comparer: Collections.IComparer[T]
) -> Iterable[T] pure
◆sort[T](
source: Iterable[T],
compare: (T, T) -> int pure
) -> Iterable[T] pure

sort_descending ​

ghul
◆sort_descending[T: Ghul.Comparable[T]](source: Iterable[T]) -> Iterable[T] pure

sort_by ​

ghul
◆sort_by[T..,K: Ghul.Comparable[K]](
source: Iterable[T],
key_selector: T.. -> K pure
) -> Iterable[T] pure

sort_by_descending ​

ghul
◆sort_by_descending[T..,K: Ghul.Comparable[K]](
source: Iterable[T],
key_selector: T.. -> K pure
) -> Iterable[T] pure

transpose and permutations both produce read-only lists built from the whole source:

ghul
let rows = [[1, 2, 3], [4, 5, 6]]
// transpose: the columns of rows, each as a list
for column in rows |> transpose() do
write_line("column: {column |> join(", ")}")
od
// permutations: every ordering of the elements
for ordering in ["a", "b", "c"] |> permutations() do
write_line("ordering: {ordering |> join("")}")
od

transpose ​

The source's rows and columns exchanged, each column a read-only list: the first column holds the first element of each row, in row order, and so on. Transposing stops at the shortest row, so a ragged source is read as its rectangular part.

ghul
◆transpose[T](rows: Iterable[Iterable[T]]) -> Pipe[List[T]]

permutations ​

Every ordering of the source's elements, each a read-only list. The orderings come in the order of the source's own positions, which is sorted order when the source is sorted.

ghul
◆permutations[T](source: Iterable[T]) -> Pipe[List[T]]

terminals ​

A terminal returns something other than a pipe, so it is where a pipe ends. They fall into three loose groups: finding a single element, collecting the elements into a container, and folding or consuming the sequence as a whole.

The searching combinators come in pairs. find-style ones take a predicate or a mapper and scan; first-style ones look only at the leading element. Each has a variant returning T? and one that throws instead:

ghul
let words = ["alpha", "beta", "gamma"]
// find scans for the first element matching a predicate
// first takes no predicate and yields the leading element
write_line("find: {words |> find(w => w.length == 4) ?? "none"}")
write_line("first: {words |> first() ?? "none"}")
// only yields the single element, and throws if the source
// holds none or more than one
write_line("only: {["solo"] |> only()}")
// a mapper that gives a result only for words longer than four
// characters
shout(w: string) -> string? pure =>
if w.length > 4 then w.to_upper() else null fi
// find_map keeps mapping until one answers; first_map maps the
// first element and gives up when that one declines
write_line("find_map: {words |> find_map(shout) ?? "none"}")
write_line("first_map: {words |> first_map(shout) ?? "none"}")
// beta is the only word the mapper declines, so leading with it
// is what separates the two
let beta_first = ["beta", "alpha", "gamma"]
write_line("find_map: {beta_first |> find_map(shout) ?? "none"}")
write_line("first_map: {beta_first |> first_map(shout) ?? "none"}")

find ​

The first element matching the predicate, absent if none does. first is the same question with no predicate.

ghul
◆find[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> T? pure

find_map ​

Calls mapper on each element in turn and returns the first present result. first_map differs: it calls the mapper on the first element only, and returns absent if the mapper returns absent for it.

ghul
◆find_map[T..,U](
source: Iterable[T],
mapper: T.. -> U? pure
) -> U? pure

find_or_throw ​

As find, throwing instead of returning absent when no element matches.

ghul
◆find_or_throw[T](
source: Iterable[T],
predicate: T -> bool pure
) -> T pure

find_map_or_throw ​

As find_map, throwing instead of returning absent when no element maps.

ghul
◆find_map_or_throw[T..,U](
source: Iterable[T],
mapper: T.. -> U? pure
) -> U pure

first ​

The leading element, absent when the source is empty.

ghul
◆first[T](source: Iterable[T]) -> T? pure

first_map ​

Calls mapper on the leading element only. Compare find_map, above, which keeps going.

ghul
◆first_map[T..,U](
source: Iterable[T],
mapper: T.. -> U? pure
) -> U? pure

first_or_throw ​

As first, throwing instead of returning absent when the source is empty.

ghul
◆first_or_throw[T](source: Iterable[T]) -> T pure

first_map_or_throw ​

As first_map, throwing instead of returning absent.

ghul
◆first_map_or_throw[T..,U](
source: Iterable[T],
mapper: T.. -> U? pure
) -> U pure

last ​

The final element, absent when the source is empty. last reads the whole source to find it.

ghul
◆last[T](source: Iterable[T]) -> T? pure

only ​

The single element the source holds, throwing when it is empty or holds more than one.

ghul
◆only[T](source: Iterable[T]) -> T pure

any ​

ghul
◆any[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> bool pure

all ​

ghul
◆all[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> bool pure

count ​

The first form counts every element. The second counts the elements the predicate accepts: numbers |> count(n => n % 2 == 1).

ghul
◆count[T](source: Iterable[T]) -> int pure
◆count[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> int pure

sum ​

Every element added together. An empty source sums to zero.

ghul
◆sum[T: INumber[T]](
source: Iterable[T]
) -> T pure

sum_by ​

The total of what selector returns for each element, zero for an empty source. sum_by(f) gives the same total as map(f) |> sum():

ghul
let words = ["pipe", "stage", "terminal"]
// sum_by totals what the function returns for each element
write_line("letters: {words |> sum_by(w => w.length)}")
// last is absent when the source is empty
write_line("last: {words |> last() ?? "none"}")
write_line("long: {words |> filter(w => w.length > 10) |> last() ?? "none"}")
ghul
◆sum_by[T..,N: INumber[N]](
source: Iterable[T],
selector: T.. -> N pure
) -> N pure

product ​

Every element multiplied together. An empty source gives one.

ghul
◆product[T: INumber[T]](
source: Iterable[T]
) -> T pure

min ​

The smallest element, absent when the source is empty.

ghul
◆min[T: Ghul.Comparable[T]](
values: Iterable[T]
) -> T? pure

max ​

ghul
◆max[T: Ghul.Comparable[T]](
values: Iterable[T]
) -> T? pure

min_by ​

ghul
◆min_by[T..,K: Ghul.Comparable[K]](
source: Iterable[T],
key_selector: T.. -> K pure
) -> T? pure

max_by ​

ghul
◆max_by[T..,K: Ghul.Comparable[K]](
source: Iterable[T],
key_selector: T.. -> K pure
) -> T? pure

The collecting combinators differ in what they hand back:

ghul
let numbers = [3, 1, 4, 1, 5, 9, 2, 6]
// collect gives back an array, collect_mutable the mutable LIST[T],
// and collect_set drops duplicates
write_line("collect: {numbers |> collect() |> join(", ")}")
write_line("collect_mutable: {numbers |> collect_mutable() |> join(", ")}")
write_line("collect_set: {numbers |> collect_set() |> join(", ")}")
// partition splits on a predicate: the matching elements first
let (even, odd) = numbers |> partition(x => x % 2 == 0)
write_line("partition: even {even |> join(", ")}, odd {odd |> join(", ")}")
// group_by keys each element, collecting the elements per key
let by_size = numbers |> group_by(x => if x < 5 then "small" else "large" fi)
write_line("group_by: small {by_size["small"] |> join(", ")}")
write_line("group_by: large {by_size["large"] |> join(", ")}")
// collect_mutable_map gives back a map that can be added to
let squares = numbers |> distinct() |> collect_mutable_map(x => x, x => x * x)
squares[10] = 100
write_line("collect_mutable_map: {squares.count} entries")

collect ​

Collects into an array, T[], which is a read-only Collections.List[T]. collect_mutable gives back the mutable LIST[T] instead, and the others collect into a set or a map.

Each collecting function is a collection constructor written as a function: collect is ARRAY(p), collect_mutable is LIST(p), collect_set is SET(p), collect_mutable_map is MAP(p), and join is string(p, separator). collect_map has no constructor of its own, because it gives back the read-only Map[K, V].

ghul
◆collect[T](source: Iterable[T]) -> T[] pure

collect_mutable ​

ghul
◆collect_mutable[T](source: Iterable[T]) -> LIST[T] pure

collect_set ​

ghul
◆collect_set[T](source: Iterable[T]) -> SET[T] pure

collect_map ​

The first form takes each element's key and value from two functions. The second collects a sequence of key and value pairs, which is how a map with fixed contents is written: [("a", 1), ("b", 2)] |> collect_map().

ghul
◆collect_map[T..,K,V](
source: Iterable[T],
key_selector: T.. -> K pure,
value_selector: T.. -> V pure
) -> Map[K,V] pure
◆collect_map[K,V](source: Iterable[(K, V)]) -> Map[K,V] pure

collect_mutable_map ​

As collect_map, giving back the mutable MAP[K, V] rather than the read-only Map[K, V], so entries can be added to it afterwards.

ghul
◆collect_mutable_map[T..,K,V](
source: Iterable[T],
key_selector: T.. -> K pure,
value_selector: T.. -> V pure
) -> MAP[K,V] pure
◆collect_mutable_map[K,V](source: Iterable[(K, V)]) -> MAP[K,V] pure

partition ​

Splits the source in two on a predicate. The elements matching the predicate come first, then the elements not matching.

ghul
◆partition[T..](
source: Iterable[T],
predicate: T.. -> bool pure
) -> (List[T], List[T]) pure

group_by ​

Collects the elements into a map, keyed by what key_selector returns for each.

ghul
◆group_by[T..,K](
source: Iterable[T],
key_selector: T.. -> K pure
) -> Map[K, List[T]] pure

reduce ​

Folds the source into a single value, starting at seed and calling accumulator with the running value and each element in turn. The second form passes the final running value through a mapper before returning it.

ghul
◆reduce[T..,TRunning](
source: Iterable[T],
seed: TRunning,
accumulator: (TRunning,T..) -> TRunning pure
) -> TRunning pure
◆reduce[T..,TRunning,TOut](
source: Iterable[T],
seed: TRunning,
accumulator: (TRunning,T..) -> TRunning pure,
mapper: (TRunning) -> TOut pure
) -> TOut pure

each ​

Calls action on every element. It doesn't return a value and, alone among these, is not pure - it exists for its side effects.

ghul
◆each[T..](source: Iterable[T], action: T.. -> void) -> void

append_to ​

Appends each element to a StringBuilder, separated by separator, or by ", " when that is left off. join does the same and returns a new string.

ghul
◆append_to[T](
source: Iterable[T],
into: System.Text.StringBuilder,
separator: string
) -> System.Text.StringBuilder
◆append_to[T](
source: Iterable[T],
into: System.Text.StringBuilder
) -> System.Text.StringBuilder

join ​

Joins the elements into one string, separated by separator, or by ", " when left off.

ghul
◆join[T](source: Iterable[T], separator: string) -> string pure
◆join[T](source: Iterable[T]) -> string pure

render_elements ​

Writes the elements in brackets, as [1, 2, 3], whatever to_string the source's own type declares. It stops at 100 elements with ..., so an unbounded pipe is written too. This is the text a pipe gives as its own to_string and in string interpolation.

ghul
◆render_elements[T](source: Iterable[T]) -> string pure

displaying values ​

The runtime formats any value as text in two ways. $(value) gives the text a program shows its user: string interpolation uses it for any value whose type gives no text of its own, as string interpolation describes. inspect(value) gives the detailed form a REPL or a debugging session wants: the same structure, with each string and character quoted wherever it appears inside a value. $ doesn't need a use, and inspect is in Ghul:

ghul
struct POINT(x: int public, y: int public)
let words = ["one", "two"]
write_line($(words))
write_line(inspect(words))
write_line(inspect("top"))
write_line($(POINT(3, 4)))
write_line(inspect((1, "one", 'c', true)))
let cycle = Collections.LIST[object]()
cycle.add(cycle)
write_line($(cycle))

At the top, a string or character is the whole answer, so both functions write it as itself. Inside a value, inspect quotes it and $ does not.

$ and inspect write a value by the first of these rules that fits:

  • They write an absent value as null, and a bool as true or false.
  • They let a type that implements Displayable write itself, as described below.
  • They write a tuple as its parts in parentheses, and a map entry as (key, value).
  • They write a value whose type declares its own to_string with that to_string, even when the value is also a sequence. The runtime's own pipes, and generators, are the exception: their to_string writes their elements.
  • They write a sequence, such as an array, a list or a pipe, as its elements in brackets.
  • They write a class, struct or union variant with no to_string of its own as its type and members, such as POINT(x = 3, y = 4).
  • They write a value of a type from another language with no to_string of its own as its .NET type name. They do not read its properties, because a property getter can run any code: reading a task's result waits for the task.

They stop a sequence after 100 elements and end it with , ...], so they can write an unbounded pipe. Stopping there leaves nothing behind for the next read of the pipe. Where a value contains itself, they write <cycle> at the point it recurs. The same value appearing in two places is not a cycle, and they write it in full both times.

A type chooses how it is displayed by implementing Displayable. Its one method writes the value through a DISPLAY_STATE. Write each child value with state.render(child) rather than $(child): the state carries the element limit and the values already being written, and a fresh call to $ starts without them. state.mode says whether the text is for $, DisplayMode.CLEAN, or for inspect, DisplayMode.DETAILED:

ghul
class SCORE(points: int): Displayable is
▲display(state: DISPLAY_STATE) is
state.append("[")
state.render(points)
state.append(if state.mode == DisplayMode.DETAILED then " points]" else "]" fi)
si
si
write_line($(SCORE(3)))
write_line(inspect(SCORE(3)))
write_line($([SCORE(1), SCORE(2)]))

A DISPLAY_STATE can also be created directly, with a mode and a different element limit, and read back with to_string() after writing into it.

display(value) shows a value while the code goes on running, rather than only at the end. A host that shows values, such as a REPL, a notebook or the playground, installs a DisplaySink, and display sends the value to it. With no host installed, display writes what inspect gives for the value as a line of standard output. display(value, id) names what it shows, and update_display(value, id) replaces what was shown under that name, which is how a cell shows progress in place. With no host there isn't a display to replace, so update_display writes another line. All three are in Ghul:

ghul
display([1, 2, 3])
display("working", "status")
update_display("done", "status")

Displayable customises the text $ and inspect produce for a value. Renderable offers other media for the same value, such as an image, which a host can show in place of that text. Its representations() method gives each as a MIME type and its content, best first:

ghul
class GREETING(name: string): Renderable is
▲representations() -> Collections.Iterable[(mime: string, content: ubyte[])] pure =>
[
(mime = "text/markdown", content = System.Text.Encoding.utf8.get_bytes("**hello** {name}")),
(mime = "text/plain", content = System.Text.Encoding.utf8.get_bytes("hello {name}"))
]
si
for (mime, content) in GREETING("world").representations() do
write_line("{mime}: {content.count} bytes")
od
text/markdown: 15 bytes
text/plain: 11 bytes

The host chooses which MIME types it shows, and the text $ writes is always the fallback. A host that shows output in a web page does not insert text/html or image/svg+xml from a value into its own document, since either can carry script.