haku reference manual
haku is a dynamically typed, pure functional programming language, used for programming brushes in rakugaki.
For the layperson, it can be thought of a beefed up calculator. It has roots in ordinary school algebra, but has a much more powerful set of features, with the special ability of being able to edit images.
Overview
haku programs are called brushes. The purpose of a brush is to produce instructions for rakugaki on how the user may interact with the wall (via reticles), and what things should be drawn on the wall as a result of those interactions (via scribbles). Collectively, reticles and scribbles are what we call effects.
A brush’s task is to compute (or in simpler terms, calculate) an effect that rakugaki will then perform.
In case of reticles, rakugaki will allow the user to interact with the wall, and then ask haku for another effect to perform afterwards (a continuation).
In case of scribbles, rakugaki will draw the scribble onto the wall, without asking haku for more.
Once rakugaki runs through all effects, we say that the brush has finished executing.
Lexical elements
Comments
haku’s most basic lexical element is the comment.
-- This is a comment.
-- こんにちは!
Comments introduce human-readable remarks into a brush’s code. Their only purpose is documentation. They serve no semantic meaning, and thus do not affect the result of the brush whatsoever.
Comments begin with --, and span until the end of the current line of text.
Once the line ends, the comment does, too.
Comments do not necessarily have to appear at the beginning of a line.
magnitude: \v -> -- calculate the magnitude of a vector
hypot vecX.v vecY.v
Literals
Literals represent literal values that are input into the brush.
haku has a few types of literals, but not all literals are purely lexical elements (some of them can nest)—which is why the different types of literals are covered under the Expressions section.
Identifiers
Identifiers are used for naming values inside a brush. New names are introduced using defs and lets. Once a name is introduced, it may be referenced using its identifier in its corresponding scope—the whole brush for defs, and the following expression in lets.
An identifier starts with a lowercase ASCII letter—a–z or an underscore—_, and is followed by zero or more ASCII letters of any case—a–z, A–Z, digits—0–9, and underscores—_.
This then may be followed by an arbitrary number of suffix characters prime symbols—' and question marks—?.
By convention, prime symbols are used in the same way they are used in math notation—for introducing a distinct variable of the same name as another variable, usually derived from the previous.
For example, given a variable named position, an updated position may be named position'.
The question mark suffix is conventionally used for boolean variables, as well as boolean-returning functions. By convention, only one question mark is always used.
Identifiers starting with uppercase ASCII letters—A–Z are not identifiers, but rather tags, and therefore cannot be used as def and let names.
The following identifiers are reserved as keywords, and have special meaning assigned within the language syntax.
-
if— Introduces anifexpression. -
else— Introduces theelseclause in anifexpression.
Additionally, the following identifiers are reserved for future use, and may not be used for naming defs and lets.
-
and -
or
By convention, a prime symbol ' suffix can be used to work around this restriction.
For example, instead of naming a variable if, try naming it if' (read as if prime, “the other if”).
Operators and punctuation
Operators and punctuation share a section in this part of the reference due to their lexical similarity.
Operators serve as a terse syntax for calling a small set of built-in functions within the program (described in detail in the system reference documentation), while punctuation serves the purpose of syntactic delimiters.
The following symbols are operators:
+ - * /
== != < <= > >= !
And the following symbols are punctuation:
<newline>
( ) [ ] ,
= :
. |
\ ->
<newline> is literally written down as a line break in programs, which would be invisible in this reference.
Structure of a brush
A brush is structured like so:
def1: expr1
def2: expr2
def3: expr3
-- ...
effectExpr
That is, there are two parts to a brush: its defs, followed by the resulting effect expression. The effect produced by this expression is the effect performed by rakugaki when the user interacts with the wall (clicks on it, touches it, starts a pen stroke).
Defs introduce names for values that are available across the entire program. They are most commonly used to name constants and functions.
Example:
-- Constant definition
pi: 3.14159265
-- Function definition
magnitude: \v ->
hypot vecX.v vecY.v
Expressions
haku is a strictly expression-oriented language. There is no idea of statements, as would be the case in lower-level languages like C++ or JavaScript. This comes from the fact that haku is a pure functional language, which means there aren’t any expressions whose result you would want to discard, only for their side effects.
Nil
An empty parenthesis represents a nil value—that is, literally a value that means “no value.”
()
It is one of the only values considered false by the language, other than the False boolean.
Numbers
Numbers in haku are written down as a sequence of ASCII digits—0–9, followed by an optional decimal point . with a decimal part.
0
123
3.14159265
Internally, they are represented by 32-bit floating point numbers. This means that they have fairly limited precision, and do not always behave exactly like math on real numbers. For example, there are magical values for ∞ and -∞ (which can exist and can be operated upon), as well as a value called NaN (not a number), which are produced as results of certain operations that aren’t well-defined in math (most commonly division by zero).
Tags
Tags are similar to identifiers, but start with an uppercase rather than a lowercase ASCII letter (that is, A–Z).
They are values which represent names.
This concept may feel a bit alien. As an example, consider how haku implements record types. From the perspective of the user, a record type acts like a function which accepts a tag as an argument—with the tag being the name of the record field.
withDotter \d ->
stroke 8 #000 (line d.From d.To)
---- -- these are tags
There are also two special tags, True and False, which are used to represent Boolean logic.
The boolean False is the only false value in the language, other than nil.
Colors
haku has a literal for representing RGBA colors. It takes one of the following four forms, from most explicit to least explicit:
#RRGGBBAA
#RRGGBB
#RGBA
#RGB
Each character in a color literal is a hexadecimal digit, with the digits 0–9 representing digits 0 to 9, and letters a–f representing digits 10 to 16 (case insensitive.)
For #RGB and #RGBA, the digits are repeated twice implicitly—that is, #1234 is the same as #11223344.
This syntax is designed to be convenient for working with colors coming from external programs. For example, you may pick a color from an online palette, and paste its hex code straight into your brush code.
Example (rakugaki logo colors):
white: #FFF
peach: #FFA6A6
raspberry: #F44096
Lists
Lists are fixed-length sequences of values.
They are written down by listing out a sequence of comma , or newline-separated items, enclosed in square brackets [].
six: [1, 2, 3, 4, 5, 6]
four: [
1
2
3
4
]
Lists are allowed to nest, as the values may be of any type—including lists themselves.
Lists are most commonly used to compose scribbles. A list is also a scribble, which draws the scribbles it contains within itself, from first to last.
Operators
Operators in haku are used mostly for basic mathematical operations.
They are either unary, written in prefix form !x, or binary, written in infix form a + b.
While order of operations in case of unary operators is unambiguous (innermost to outermost), infix operators are not as simple. Certain infix operators have precedence over others.
This precedence depends on the operator used, as well as the spacing around it.
Spacing only matters for infix operators; prefix operators may have any amount of spaces around them, though conventionally they are glued to the expression on the right, like (-1), or vec -1 0.
Infix operators with spaces around them are classified as loose, and those without spaces around them are tight. An unequal amount of spaces around an infix operator is considered an error (or is parsed as a prefix operator, depending on context).
Based on these two groups, the precedence of operators is defined as follows:
-
Prefix operators:
-a,!a -
Tight
-
Dot:
a.b -
Arithmetic:
a+b,a-b,a*b,a/b -
Comparisons:
a==b,a!=b,a<b,a<=b,a>b,a>=b
-
Dot:
-
Function calls:
a b -
Loose
-
Dot:
a . b -
Arithmetic:
a + b,a - b,a * b,a / b,a |b -
Comparisons:
a == b,a != b -
Variables:
a: b,a = b
-
Dot:
The operators +, -, *, /, ==, !=, <, <=, >, >=, !, are used for calling functions built into the haku system library.
Other infix tokens listed above have other semantic meaning.
-
a b,., and|— Used for calling functions. -
:— Used for introducing defs. -
=— Used for introducing lets.
Examples of how these precedence rules work in practice:
2 + 2 * 2 == 8 -- left to right
2 + 2*2 == 6 -- 2*2 goes first
2+2 * 2 == 8 -- 2+2 goes first
2+2*2 == 8 -- left to right
sin 2 * pi * x == (sin 2) * pi * x -- function call goes first
sin 2*pi*x == sin (2 * pi * x) -- argument goes first
-- unintuitive for users of C-likes:
-- prefix `-` has higher precedence than `.`
-vecX.v == (-vecX).v
One thing that should be noted about haku’s operator precedence is that, unlike math notation and most other programming languages, +, -, *, and /, are evaluated from left to right.
This is because otherwise they would interact unintuitively with the pipe | operator, which is effectively used as an operator that turns any function infix.
It is not obvious where | would sit in the precedence hierarchy if arithmetic was split into separate precedence levels for + and -, and * and /, whereas with haku’s solution, all arithmetic expressions are simply read from left to right.
Parentheses
In case the tight-loose system is not expressive enough, parentheses can be used as an escape hatch for grouping expressions.
2 + (2 * 2) == 2 + 2*2
Functions
Functions in haku follow the definition of mathematical functions: given a set of arguments, the arguments are substituted into the function’s parameter variables, and a result is computed from the resulting expression.
A function literal is written down like \params -> result, where params is a comma-separated list of parameters, and result is the function’s resulting expression.
square: \x -> x * x
A newline is permitted after the arrow ->, which should be preferred for most functions.
magnitude: \v ->
hypot vecX.v vecY.v
normalize: \v ->
l = magnitude v
v / vec l l
Note that there must be at least one parameter. In case you need a function with no parameters, you almost always want a constant value instead.
Functions can be used by calling them with space-separated arguments. Note that space-separated function calls have higher precedence than most arithmetic operators, which means you have to use parentheses or tighten the expression to pass more complicated expressions.
normalize (vec 4 4)
Note that a call must pass in exactly the amount of arguments defined by the function.
Calling the above-defined normalize with more than one argument will not work:
normalize (vec 4 4) (vec 5 5)
In places where a call with space-separated arguments is inconvenient, there are two alternative syntaxes for calling a function.
The first is by using the . operator, which, when used tightly, can be used to do a function call with higher operator precedence than an ordinary space-separated call would have.
f x == f.x
Combined with records and tags, it mimics the record field access syntax found in C-like programming languages.
withDotter \d ->
stroke 8 #000 (line d.From d.To)
------ ----
The other alternative syntax is the pipe | operator.
It calls the function on the right with the argument on the left.
2 |sqrt
When a space-separated function call is found on the right, the | operator instead inserts the value from the left as the first argument of the function call.
x |mod 2 == mod x 2
The spacing convention around | is a bit unusual, but the above example demonstrates why: the | operator effectively turns an arbitrary function into an infix operator on par with built-in arithmetic operators.
Therefore, the function name is glued to the pipe, like |mod, to appear as one word visually.
Certain functions are built-in, and implement core functionality that cannot be implemented in haku alone (at all, or in a performant manner). The haku system library is what defines all the built-in functions.
Due to temporary limitations of the implementation, built-in functions cannot be referenced like regular functions. They always have to appear in a call.
If you’d like to reference a built-in function to e.g. pass it to a list-transforming function, you will have to wrap it in a function of your own:
add: \x, y -> x + y
sum: [1, 2, 3] |reduce 0 add
sin': \x -> sin x
sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin'
if expressions
if expressions allow for choosing between two different results based on a condition.
if (cond)
a
else
b
When cond is true, a will be computed and returned as the result.
Otherwise, b will be computed and returned as the result.
Note that in both cases, only one of the two expressions is computed. This allows for implementing bounded recursion to achieve repetition.
-- Fibonacci sequence
fib: \x ->
if (x > 1)
fib n-1 + fib n-2
else
x
Let expressions
Let expressions introduce a new variable, or let, that can be referenced in the expression on the next line.
x = y
expr
The difference between lets and defs is that the value of a let can change, because it can depend on non-def values, such as function arguments (therefore making it variable.)
This however means that lets have reduced scope. The name introduced by a def can be used in the entire program—even before the line it’s introduced on—while the name introduced by a let can only be used in the expression that immediately follows the let.
x: 1
f: \y ->
z = x + 1
x + y + z -- ok
z -- not ok
This also means that lets cannot be used to create recursive functions, because the name introduced by a let only becomes visible in its following expression.
-- This is okay:
defib: \x ->
if (x > 1)
defib n-1 + defib n-2
else
x
-- This will not work:
letfib = \x ->
if (x > 1)
-- Because letfib is not yet defined at this point.
letfib n-1 + letfib n-2
else
x
defib 5 + letfib 5 -- It is only defined in this expression.
Since a let can be followed up by any other expression, multiple lets can be chained together.
x = 4
y = x + 3
x + y
Note however that x + y finishes the chain of lets, which means we cannot introduce additional ones after that line.
That would begin another expression!
Types
haku distinguishes values between a few different types.
- nil
-
tag
-
boolean - either
FalseorTrue. Indicates truth or falsehood, used inifconditions.
-
boolean - either
- number
-
vec - a 4-dimensional vector, composed of four
numbers. -
rgba - an RGBA color, composed of four
numbers. This is the type of color literals. - function
- list
- shape - a mathematical shape.
-
effect - an action that can be performed by rakugaki.
- scribble - something that can be drawn on the wall.
- reticle - an interaction the user can make with the wall.
These types are incompatible with each other. If you pass in a tag to a value that expects a number, you will get an error.
You can refer to the system library documentation for more information on the types accepted by functions.
Note that it has a more precise notation for describing types, which explicitly documents the types of values that will be found in more nuanced situations, such as the map function.
Truth
Conditions in if expressions, as well as the ! operator, consider certain types of values truthy, and others falsy.
Falsy values include nil and the boolean False.
All other values are considered truthy.