Basic Syntax
Statements
Halcyon is part of the ML family of languages - the most well known example of which is O'Caml. The language is not whitespace sensitive. Statements do not end with a semicolon, instead they begin with a keyword:
| Keyword | Usage |
|---|---|
module |
Define a new module |
let |
Define a new variable |
type |
Define a new type or type alias |
do |
Execute some expression |
trait |
Define a new trait (an "interface" in OOP terms, "type class" in FP terms) |
impl |
Implement a trait for some type(s) |
import |
Import the contents of another file (like #include in C) |
use |
Add the contents of a module to the current namespace |
bundle |
Define the name of the current bundle (package) |
Example:
bundle example
module animals =
type Cat = {
name: String,
owner: String,
}
trait Animal: t =
let speak: t -> String
end
impl Animal Cat =
let speak =
fn cat => cat.name + " says: Meow!"
end
let my_cat: Cat = {
name = "Carl",
owner = "Logan",
}
do speak my_cat
endPrimitive Types
There are 7 primitive types in Halcyon
| Type | Example |
|---|---|
| Unit | () |
| Boolean | true false |
| Real | 3.14159 2e4 |
| Integer | 42 0xFF 0o777 0b101 |
| Natural | 1n 0xEFn 0x523n 0b111n |
| String | "Hello, world!\n" |
| Glyph | 'a' 'あ' '☺' |
The unit is similar to void in C; it can be thought of as "nothing" or "no value".
Gylphs are similar to characters, except they can be any UTF-8 codepoint, and are 32 bits wide instead of 8.
Strings and glyphs can contain escape sequences, these are:
| Escape | Meaning |
|---|---|
| \n | Newline |
| \r | Line feed |
| \t | Tab |
| \b | Backspace |
| \\ | Backslash |
| \" | Double quote |
| \' | Single quote |
| \xFF | Raw byte |
| \w0FFF | Raw word |
Tuples and Arrays
A tuple is a fixed length list.
The values in a tuple do not need to be the same type.
The Unit type just refers to an empty tuple.
let tup: (Integer, Real, String, Glyph) = (1, 2.0, "foo", 'a')An array is a variable length list. Arrays can only contain one type of value.
let pie_ingredients = [
"sugar",
"flour",
"butter",
"apples",
]
let sandwhich_ingredients = [
"bread",
"turkey",
"lettuce",
"cheese",
]The .. operator adds the contents of one list to another.
-- A new list containing everything in the previous lists,
-- plus 2 extra items
let grocery_list = [
..pie_ingredients,
..sandwhich_ingredients,
"chips",
"salsa",
]Functions
A function is written like: fn <parameters> => <body>.
The body of a function is a single expression.
There is no return statement in Halcyon, a function returns the value produced by its body.
let add_2_nums =
fn a b => a + bCalling a function is simple, usually no parenthesis are necessary:
let five =
add_2_nums 2 3Function calls have a very high precedence.
In the order of operations, a function call almost always happens first.
This means that if an argument is an expression like a + b, that expression needs to be wrapped in parenthesis.
Arguments should be wrapped individually, not together.
do add_2_nums 5 (4 * 3)
-- Produces 17
do add_2_nums 5 4 * 3
-- Produces 27
-- The same as: (add_2_nums 5 4) * 3
do add_2_nums(5 4 * 3)
-- Error: 5 is not a function!
-- This is parsed as: add_2_nums(5(4 * 3))
do add_2_nums(5, 4 * 3)
-- Error: add_2_nums expects a number, (5, 4 * 3) is a tupleThe |> operator (called the pipe operator) is another way to call functions.
It calls the function to its right with the argument to its right.
do add_2_nums 3 5
|> show -- Convert to string
|> println -- Print the string to console
let push = fn number array => [..array, number]
-- |> can be combined with the normal function call syntax
-- Normal function calls happen first
let list = []
|> push 1
|> push 2
|> push 3
-- list = [1, 2, 3]Operators
Halcyon has the standard arithmetic and logic operators, in addition to several operators that work on functions.
| Operator | Usage |
|---|---|
+ - * / |
Arithmetic |
and or xor not |
Bitwise operations |
|> |
Function application |
>> << |
Function composition (left and right) |
+> |
Map |
*> |
Flat Map |
Operators in Halcyon are just functions.
An operator can be re-defined using [op] syntax, where op is the operators symbol.
This is called "boxed operator" syntax.
The new definition takes effect for the scope of the definition.
-- Swap addition and subtraction
let [+] = core::ops::[-]
let [-] = core::ops::[+]
let a = 5 + 3
-- a = 2
let b = 8 - 1
-- b = 9
-- Define `*` as array concatenation
let [*] = fn a b => [..a, ..b]
let list = [0, 1] * [2, 3]
-- list = [0, 1, 2, 3]
[-]refers to the binary operatora - b, while[~]refers to the unary operator-a
Boxed operators can be used directly in expressions as well. They are treated like regular functions.
let ten = [/] 30 3
-- Calls the `/` function with `30` and `3`,
-- equivalent to `30 / 3`The >> and << operators are for function composition, they effectively combine two functions together.
(f >> g) v is the same as g (f v), while (f << g) v is the same as f (g v).
let [>>] = fn a b c => b (a c)
let [<<] = fn a b c => a (b c)Patterns and Conditionals
Halcyon has two branching expressions: if _ then _ else _ and match _ with _.
If expressions test if a value is true or false, and always have two branches.
Since if is an expression that must produce a value, it is not allowed to omit the else branch.
A match expression is like a switch statement, but more powerful.
It compares a value against patterns, taking the first branch with a matching pattern.
There is no need for break, branches do not fall through.
-- Subtraction which does not return < 0
let saturating_subtraction = fn a b =>
if a < b then
0
else
a - b
let fizz_buzz = fn num =>
match (num mod 3, num mod 5) with
| (0, 0) => "FizzBuzz"
| (0, _) => "Fizz"
| (_, 0) => "Buzz"
-- Otherwise, return the number as a string
| (_, _) => show numPatterns describe the shape of a value, and match with values that fit this shape.
An identifier inside of a pattern defines a new variable.
The _ symbol is called a "blank", it matches with anything and just ignores its value.
Patterns appear in match expressions and let statements:
let (a, _) = (1, 2)
-- a = 1,
-- 2 is ignored
let { owner, name: cats_name } = my_cat
-- owner = "Logan", cats_name = "Carl"
let array_length = fn array =>
match array with
-- Match empty arrays
| [] => 0
-- Match non-empty arrays:
-- Assign the first element of the array to `first`,
-- and the rest of the elements to `rest`
| [_, ..rest] => 1 + (array_length rest)In practice, it is very common to write functions with a single argument that match on that argument.
Halcyon has a special syntax to make these functions more concise.
The function array_length from before could also be written like:
let array_length = fn
| [] => 0
| [_, ..rest] => 1 + (array_length rest) Loops
Instead of for or while loops, Halcyon uses recursive function calls.
Tail-call optimization prevents stack overflows most of the time.
TODO write an explanation of tail calls.
Comments
The two kinds of comments in Halcyon are block comments (* ... *) and line comments beginning with --.
Block comments can be nested, like (* (* (* foo *) *) *)