4.9. Patterns
Morloc’s pattern functions are first-class getters, setters, and bracket
operators for reaching into and rearranging data structures. They are ordinary
values, so you can pass them around, map them over a list, and compose them
like any other function.
This section is about extracting and rebuilding data. To bind a value’s parts to names, or to dispatch a function on the shape of its arguments, see Pattern Matching instead.
The examples below use anonymous record types, which are written with = rather
than :::
pts :: [{x = Int, y = Int}]
pts = [{x=0, y=100}, {x=1, y=101}, {x=2, y=102}, {x=3, y=103}]
Using :: inside a record type is a common slip, and the compiler says so:
pat.loc:8:9: type-level record literals use `=` to bind fields, not `::`
try: {x = <type>, ...}
`::` is for declarations (e.g. `x :: Int`, `record R where { x :: Int }`)
4.9.1. Getter patterns
A getter describes an optionally branching path into a structure. Each segment is a tuple index, a record key, or a group of them. Terminal positions come back as a tuple.
-- the 1st element of a tuple of any size
.0 (1,2) -- 1
.0 ((1,3),2,5) -- (1,3)
-- the 2nd element of the first element
.0.1 ((1,3),2,5) -- 3
-- the 2nd and 1st elements, in that order
.(.1,.0) (1,2,3) -- (2,1)
.(.1,.0) (1,2) -- (2,1)
-- indices and keys mix freely
.0.(.x, .y.1) ({x=1, y=(1,2), z=3}, 6) -- (1,2)
A pattern is a function, so it goes wherever a function goes:
map .1 [(1,2),(2,3)] -- [2,3]
4.9.2. Setter patterns
A setter is the same path with an assignment at each terminus:
.(.0 = 99) (1,2)
.0.(.x=99, .y.1=33) ({x=1, y=(1,2), z=3}, 6)
$ ./patterns s1
[99,2]
$ ./patterns s2
[{"x":99,"y":[1,33],"z":3},6]
Setters do not mutate. The spine of the structure is copied, and unmodified
fields still point at the original data. So .(.0 = 42) x builds a new tuple
whose first field is 42 and whose remaining fields are the original elements.
Records behave the same way.
4.9.3. Bracket patterns
Lists get a dedicated bracket form with Python’s index and slice syntax, written
after a dot: .[i] picks an element, .[i:j] takes a sub-range, and .[i:j:k]
adds a stride. The semantics track Python — negative indices count from the
end, out-of-range bounds are clamped, and .[::-1] reverses.
ten :: [Int]
ten = [0,1,2,3,4,5,6,7,8,9]
$ ./patterns b1 -- .[0] ten
0
$ ./patterns b2 -- .[-1] ten negative index counts from the end
9
$ ./patterns b3 -- .[1+1] ten any expression of an IndexLike type
2
$ ./patterns b4 -- .[2:5] ten
[2,3,4]
$ ./patterns b5 -- .[:3] ten omitted start defaults to 0
[0,1,2]
$ ./patterns b6 -- .[7:] ten omitted stop defaults to length
[7,8,9]
$ ./patterns b7 -- .[:] ten no-op copy
[0,1,2,3,4,5,6,7,8,9]
$ ./patterns b8 -- .[8:99] ten bounds are clamped
[8,9]
$ ./patterns b9 -- .[0:-1] ten Python-style negative stop
[0,1,2,3,4,5,6,7,8]
$ ./patterns b10 -- .[::2] ten every other element
[0,2,4,6,8]
$ ./patterns b11 -- .[::-1] ten full reverse
[9,8,7,6,5,4,3,2,1,0]
$ ./patterns b12 -- .[7:2:-2] ten strided reverse slice
[7,5,3]
Any integral type can be an index or a bound. The conversion to the underlying
64-bit width dispatches through the IndexLike typeclass, so mixed widths are
fine:
ix :: I8 -> U32 -> [Int]
ix i j = .[(i :: I8) : (j :: U32)] ten
Composing brackets with other patterns
Brackets chain with the other pattern forms. The rule depends on whether the bracket selects one element or a list:
-
.[i].tail xs— an index yields a scalar, so the tail composes directly..[0].x ptsis(.x . .[0]) pts. -
.[i:j].tail xs— a slice yields a list, so the tail is lifted withmap..[0:3].x ptsismap .x (.[0:3] pts).
The tail can be any pattern body: a record key, a tuple index, a grouped selector, or another bracket. Nested brackets follow the same rule, with the outer map running the inner bracket on each row.
rows :: [{a = [(Int,Int)], b = [{x = Int, y = Int}]}]
rows = [ {a = [(10,20)], b = [{x=100, y=200}]}
, {a = [(30,40)], b = [{x=300, y=400}]} ]
xss :: [[Int]]
xss = [[1,2,3,4,5], [6,7,8,9,10]]
$ ./patterns c1 -- .[0].x pts scalar tail composes directly
0
$ ./patterns c2 -- .[2].y pts
102
$ ./patterns c3 -- .[-1].x pts
3
$ ./patterns c4 -- .[:3].x pts slice + field, map-lifted
[0,1,2]
$ ./patterns c5 -- .[::-1].x pts
[3,2,1,0]
$ ./patterns c6 -- .[0:3].(.x, .y) pts slice + group tail
[[0,100],[1,101],[2,102]]
$ ./patterns c7 -- .[0:2].[0:3] xss slice + nested slice
[[1,2,3],[6,7,8]]
$ ./patterns c8 -- .[0:2].(.a.[0].0, .b.[0].y) rows deep mixed chain
[[10,200],[30,400]]
Remember that these are JSON outputs, so a tuple prints as an array: c6
returns three two-tuples, which JSON shows as [[0,100],…].
Brackets are getters only. There is no setter form (.[i] = v $ xs) in this
release, and multi-axis brackets (.[i,j] for matrices and tensors) are not
available yet either. Both are planned.
4.9.4. Patterns next to Python
| Pattern | Python | Note |
|---|---|---|
|
|
patterns are functions |
|
|
|
|
|
|
|
|
|
|
|
higher order |
|
|
but non-mutating |
|
|
scalar result |
|
|
slice result (list) |
|
|
full reverse |
|
|
tail map-lifted over slice |
4.9.5. Adding bracket support to your own types
Bracket syntax is not hardcoded. It dispatches through ordinary typeclasses
declared in the standard library’s internal module, so a new container type
can opt in:
-- Indexing: .[i] xs
class Indexable f where
__access_index__ :: ?I64 -> f a -> a
-- Slicing for shape-preserving containers (List, Str, ...)
class Sliceable f where
__get_slice__ :: ?I64 -> ?I64 -> ?I64 -> f a -> f a
-- Slicing for Nat-parameterized containers (Vector, Tensor, ...) where the
-- output length differs from the input length
class SliceableDim f where
__get_slice_dim__ :: ?I64 -> ?I64 -> ?I64 -> f n a -> f m a
-- Casting any user expression in a bound position to ?I64
class IndexLike i where
__to_index__ :: i -> ?I64
For a plain container, source an Indexable and a Sliceable instance per
target language. For a container parameterized by a dimensional Nat such as
Vector n a, source Indexable and SliceableDim instead; the compiler picks
SliceableDim automatically when Sliceable is absent.
If someone passes an expression of a custom integer-like type at a bound
position, the compiler casts it through that type’s IndexLike instance. So you
can extend bracket syntax to accept new bound types — a Char index, a
fixed-point coordinate — by adding an IndexLike instance whose to_index
returns ?I64. Pass Null through as Nothing so it composes with the omitted
positions in .[i:], .[:j], and .[::].
Because the dispatch lives in libraries rather than in the compiler, a module that does not import the standard library is free to substitute a different typeclass hierarchy. Bracket syntax simply errors at codegen if no matching instance is in scope.