Back to docs

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:

example.hc
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
end

Primitive 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.

example.hc
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.

example.hc
let pie_ingredients = [
  "sugar",
  "flour",
  "butter",
  "apples",
]

let sandwhich_ingredients = [
  "bread",
  "turkey",
  "lettuce",
  "cheese",
]

The .. operator adds the contents of one list to another.

example.hc
-- 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.

example.hc
let add_2_nums =
  fn a b => a + b

Calling a function is simple, usually no parenthesis are necessary:

example.hc
let five =
  add_2_nums 2 3

Function 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.

example.hc
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 tuple

The |> operator (called the pipe operator) is another way to call functions. It calls the function to its right with the argument to its right.

example.hc
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.

example.hc
-- 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 operator a - b, while [~] refers to the unary operator -a

Boxed operators can be used directly in expressions as well. They are treated like regular functions.

example.hc
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).

example.hc
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.

example.hc
-- 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 num

Patterns 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:

example.hc
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:

example.hc
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 *) *) *)