Contents
1. Getting started
A scrap is a program: one expression, which evaluates to a value. With Go installed, install the scrap tool:
$ go install github.com/Victorystick/scrapscript/cmd/scrap@latest
It reads a scrap from standard input. scrap eval prints its value, and scrap type its type:
$ echo '1 + 2' | scrap eval
3
$ echo '1 + 2' | scrap type
int
Every example on this page is a scrap, followed by what scrap eval prints (after ⇒) and what scrap type prints (after :):
1 + 2
3int
The examples are editable: change one and press Run to run it in your browser, or try your own scraps in the playground. To run one with scrap, save it to a file and run scrap eval < file.scrap.
2. Numbers, text and bytes
Arithmetic has the usual precedence. Whole numbers are ints, and numbers with a decimal point are floats:
1 + 2 * 3
7int
1.5 * 4.0
6.0float
The two never mix. Mixing them is a type error, which scrap reports before running anything:
1 + 1.5
error: cannot unify 'int' with 'float'
1: 1 + 1.5
~~~~~~~
Convert explicitly with to-float, and back with round, floor or ceil:
to-float 1 + 1.5
2.5float
An operator needs spaces on both sides. Without them, a-b is one name, as names may contain dashes, and -1 is a negative number. There is no division operator yet; tags show how to divide.
Text goes in double quotes, and ++ joins it:
"hello" ++ ", " ++ "world"
"hello, world"text
Built-in functions come in namespaces, such as text/ and int/. You call a function by writing its argument after it, with a space between:
int/to-text 42 ++ " is " ++ "the answer"
"42 is the answer"text
A byte is ~ and two hex digits, such as ~ff, and ~~ starts base64-encoded bytes:
bytes/to-utf8-text ~~aGVsbG8=
"hello"text
Finally, () is the hole, a value that carries nothing.
3. Names
A where-clause names values for the expression before it. Each binding starts with ;:
greeting ++ ", " ++ name
; greeting = "hello"
; name = "world"
"hello, world"text
The bindings of a where-clause may refer to each other, in any order:
total
; total = price * count
; price = 3
; count = 4
12int
A scrap has no names it doesn't bind itself, apart from the built-in functions, so it means the same thing wherever it runs:
x + 1
error: unbound variable: x
1: x + 1
~
Comments start with -- and a space, and run to the end of the line.
4. Functions
A function is a parameter, an arrow and a body. It is a value like any other, so it prints as itself:
x -> x + 1
x -> x + 1int -> int
Apply it by writing an argument after it:
shout "hi"
; shout = s -> s ++ "!"
"hi!"text
A function takes one argument, so a function of two takes the first and returns a function of the second:
add 2 3
; add = a -> b -> a + b
5int
Give it fewer arguments, and you get a function that waits for the rest. It prints with the bindings it needs:
add 1
; add = a -> b -> a + b
add 1 ; add = a -> b -> a + bint -> int
Functions can take functions. In the type of twice, a is a type variable: it works for any type, as long as each a is the same one.
twice
; twice = f -> x -> f (f x)
twice ; twice = f -> x -> f (f x)(a -> a) -> a -> a
twice (x -> x * 10) 5
; twice = f -> x -> f (f x)
500int
A function that ignores its argument names it _, as in _ -> 0.
5. Pipes
x |> f is f x. Pipes read left to right, so a chain of steps reads in the order it runs:
"a,b,c" |> text/split "," |> list/length
3int
f <| x is f x too, which saves parentheses around a long argument:
list/length <| text/split "," "a,b,c"
3int
f >> g composes two functions into one that applies f and then g. g << f is the same function.
count "a,b,c"
; count = text/split "," >> list/length
3int
The last step of a pipe may be a function written in place:
3 |> x -> x * x
9int
6. Lists
A list holds any number of values of one type:
[1, 2, 3]
[ 1, 2, 3 ]list int
[1, "two"]
error: cannot unify 'int' with 'text'
1: [1, "two"]
~~~~~
>+ adds an item to the front, +< to the back, and ++ joins two lists. The arrow points at the list.
0 >+ [1, 2] +< 3
[ 0, 1, 2, 3 ]list int
[1, 2] ++ [3]
[ 1, 2, 3 ]list int
list/map applies a function to each item:
[1, 2, 3] |> list/map (x -> x * x)
[ 1, 4, 9 ]list int
list/fold combines the items into one value, starting from its first argument:
[1, 2, 3, 4] |> list/fold 0 (sum -> x -> sum + x)
10int
There are others, such as list/length, list/sort, list/repeat and text/join:
list/repeat 3 "ho" |> text/join " "
"ho ho ho"text
list/sort int/cmp [3, 1, 2]
[ 1, 2, 3 ]list int
7. Records
A record holds values by name. Its fields print in alphabetical order:
{ name = "Ada", born = 1815 }
{ born = 1815, name = "Ada" }{ born : int, name : text }
A field named like the value it holds needs no =: { name } is short for { name = name }.
person "Ada" 1815
; person = name -> born -> { name, born }
{ born = 1815, name = "Ada" }{ born : int, name : text }
Read a field with a dot, with no space before it:
ada.name
; ada = { name = "Ada", born = 1815 }
"Ada"text
A function that reads a field takes any record that has it. In its type, ..b stands for the other fields, whatever they are:
person -> person.name
person -> person.name{ name : a, ..b } -> a
Records never change, but .. makes a copy with some fields replaced. It must come first:
{ ..ada, name = "Ada Lovelace" }
; ada = { name = "Ada", born = 1815 }
{ born = 1815, name = "Ada Lovelace" }{ born : int, name : text }
A copy has the same fields as the original, of the same types, so it can't add one:
{ ..ada, died = 1852 }
; ada = { name = "Ada", born = 1815 }
error: cannot unify '{ born : int, name : text }' with '{ died : int, ..a }'
1: { ..ada, died = 1852 }
~~~~~~~~~~~~~~~~~~~~~~
9. Matching
A match function is a list of arms, each |, a pattern, an arrow and a body. Applied to a value, it takes the first arm whose pattern fits. _ fits anything.
describe 2
; describe =
| 0 -> "none"
| 1 -> "one"
| _ -> "many"
"many"text
Matching is how a scrap makes choices: there is no if. A pattern names the parts it takes apart, such as the payload of a tag or the fields of a record. As in a record, { w, h } is short for { w = w, h = h }, naming each field's value after the field:
area
; area =
| #square side -> side * side
| #rect { w, h } -> w * h
area
; area =
| #square side -> side * side
| #rect { w, h } -> w * h[ #rect { h : int, w : int }, #square int ] -> int
[#square 2, #rect { w = 2, h = 3 }] |> list/map area
; area =
| #square side -> side * side
| #rect { w, h } -> w * h
[ 4, 6 ]list int
The type of area has no ..a: it takes those two tags only. Give it another, and the type checker stops you before the scrap runs:
area (#circle 1)
; area =
| #square side -> side * side
| #rect { w, h } -> w * h
error: cannot unify '[ #rect { h : int, w : int }, #square int ]' with '[ #circle int, ..a ]'
1: area (#circle 1)
~~~~~~~~~~~~~~~
The other way round holds too: a match must have an arm for every value it can be given, so a scrap that type checks never fails to match.
| 0 -> "none"
| 1 -> "one"
error: match is not exhaustive: no arm matches _
1: | 0 -> "none"
~~~~~~~~~~~~~
Pipe a value into a match function to choose on it in place:
int/div 7 0 |>
| #some n -> n
| #none -> 0
0int
Patterns take lists apart too: [] fits the empty list, [x] a list of one item, and first >+ rest any other, naming its first item and the rest.
| [] -> "empty"
| [x] -> "one"
| first >+ rest -> "more"
| [] -> "empty" | [ x ] -> "one" | first >+ rest -> "more"list a -> text
An arm can match several arguments, one pattern per argument. This is how to combine booleans:
both #true #false
; both =
| #true -> #true -> #true
| _ -> _ -> #false
#false[ #false, #true, ..a ]
10. Recursion
Bindings can refer to themselves, so a function can call itself. Together with matching, that is how a scrap loops:
sum [1, 2, 3]
; sum =
| [] -> 0
| x >+ rest -> x + sum rest
6int
factorial 10
; factorial =
| 0 -> 1
| n -> n * factorial (n - 1)
3628800int
The bindings of a where-clause can call each other, too:
is-even 10
; is-even =
| 0 -> #true
| n -> is-odd (n - 1)
; is-odd =
| 0 -> #false
| n -> is-even (n - 1)
#true[ #false, #true, ..a ]
Only functions may be recursive. A scrap evaluates each value before using it, so a value made from itself would never be done:
ones
; ones = 1 >+ ones
error: 'ones' is recursive, so it must be a function
1: ones
~~~~
11. Types
The type checker works out the type of every scrap, so you rarely write types. You may annotate a binding with one, between its name and =, and the checker holds the value to it:
inc 1
; inc : int -> text = x -> x + 1
error: cannot unify 'int' with 'text'
2: ; inc : int -> text = x -> x + 1
~~~~~
Annotations choose too: +, - and * work on ints unless something says float.
add
; add : float -> float -> float = a -> b -> a + b
add ; add : float -> float -> float = a -> b -> a + bfloat -> float -> float
Type variables are introduced with =>, before the type that uses them:
first [3, 2, 1]
; first : a => list a -> [#some a, #none] =
| [] -> #none
| x >+ _ -> #some x
#some 3[ #none, #some int ]
A binding can name a type, too. A type that takes a type is a function of types, written with =>, and the checker prints types with the names you gave them:
half 7
; half : int -> option int = n -> int/div n 2
; option = a => [#some a, #none]
#some 3option int ; option = a => [ #none, #some a ]
A binding annotated with type names a type that takes none. A type may refer to itself through a tag's payload, as a tree does. The checker doesn't guess such types, so a function over one needs its annotation:
size (#node { left = #leaf, right = #node { left = #leaf, right = #leaf } })
; size : tree -> int =
| #leaf -> 0
| #node n -> 1 + size n.left + size n.right
; tree : type = [#leaf, #node { left : tree, right : tree }]
2int
12. Tests
scrap test runs the test cases in a scrap's value, in records and lists. A case is a tag: #pass, #fail with the reason, or #eq with a value that you got and one that you want:
{ adds = eq (1 + 1) 2
, joins = eq ("a" ++ "b") "abc"
}
; eq = got -> want -> #eq { got, want }
FAIL joins
got "ab"
want "abc"
1 failed, 1 passed
It exits with status 1 if any case failed, so it fits in scripts.
13. Scraps and the yard
Every scrap is known by its hash, the SHA-256 of its source, which scrap hash prints. A scrap refers to another by its hash, as $source: and 64 hex digits. The yard stores scraps by hash, and scrap fetches them from it. This scrap greets people. Its value is a function, which prints as the reference, and scrap type fetches it to work out its type:
$source:71d9fe524fc0ab11b9ee6e6cfa3bc3d410a4c1b3884bc1b0256c51699730b1eb
$source:71d9fe524fc0ab11b9ee6e6cfa3bc3d410a4c1b3884bc1b0256c51699730b1eb[ #cowboy, #friend { name : text, times : int }, #ron int, #stranger text ] -> text
A hash refers to that scrap and no other: change a byte, and the hash changes too. So a reference means the same thing forever, and you use it like the value it refers to:
#ron 3 |> $source:71d9fe524fc0ab11b9ee6e6cfa3bc3d410a4c1b3884bc1b0256c51699730b1eb
"hi aaaron"text
Drafts
While you work on scraps, refer to them by path instead, relative to the scrap that refers to them. With double.scrap holding x -> x * 2:
$ echo '$"./double.scrap" 21' | scrap eval
42
A scrap with paths is a draft, which only you can run. Paths resolve from the current directory, or the directory that --root names.
Publishing
scrap push stores a scrap in the yard, which type checks it first, and prints its hash:
$ scrap push < double.scrap
19ec0ec5d05c7010e3d32d79465bb7e2ac947025821e52cc9235485d4587376f
Push the scraps that a draft refers to first, then replace each path with $source: and its hash, and push the draft. Anyone can then run it by its hash.
Results are scraps
What scrap eval prints is itself a scrap, which evaluates to the same value. scrap eval apply evaluates its input, and passes the result to the function you give it, so evaluations chain:
$ echo '[1, 2, 3]' \
| scrap eval apply 'list/map (x -> x * 10)' \
| scrap eval apply 'list/fold 0 (sum -> x -> sum + x)'
60
Where next
The specification covers the whole language, though not all of it works yet. lib/avl.scrap is a larger scrap to read: a map from keys to values, which exports its types next to its functions.