Tutorial

Learn the language of scrap one piece at a time, from numbers to publishing scraps of your own.

Contents

  1. Getting started
  2. Numbers, text and bytes
  3. Names
  4. Functions
  5. Pipes
  6. Lists
  7. Records
  8. Tags
  9. Matching
  10. Recursion
  11. Types
  12. Tests
  13. Scraps and the yard

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 }
       ~~~~~~~~~~~~~~~~~~~~~~

8. Tags

A tag is a name that starts with #. It needs no declaring: you just use it.

#true
#true[ #true, ..a ]

Its type lists the tags it may be, here only #true, and ..a leaves room for others. So tags of one list may differ:

[#red, #green, #blue]
[ #red, #green, #blue ]list [ #blue, #green, #red, ..a ]

A tag may carry one value, its payload:

[#square 2, #rect { w = 2, h = 3 }]
[ #square 2, #rect { h = 3, w = 2 } ]list [ #rect { h : int, w : int }, #square int, ..a ]

Tags stand in for booleans, #true and #false, and for missing values. int/div divides, unless you divide by zero:

int/div 7 2
#some 3[ #none, #some int ]
int/div 7 0
#none[ #none, #some int ]

And comparisons return #lt, #eq or #gt:

int/cmp 1 2
#lt[ #eq, #gt, #lt ]

To get at a payload, or do something different for each tag, you match on it.

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.