Back to docs

Types

Primitives

As discussed in the syntax section, Halcyon has 6 primitive types:

  • () (equivalent to void)
  • Real (equivalent to float)
  • Integer
  • Natural (equivalent to unsigned int)
  • Boolean
  • String
  • Glyph (somewhat equivalent to char)

Assertions

Most of the time, the compiler does not need to be told what type something is. Specifying the type of a variable can still be useful sometimes. Variables defined with let may have a type assertion. The compiler checks that the type of the variable matches exactly with the asserted type. Type assertions are useful as a sanity check, and as a kind of documentation.

example.hc
let sum: Integer = 4 + 5

let sum2: Integer = 1.0 + 3.0
-- Error: Real != Integer

-- Type assertions can be anywhere in a pattern,
-- even nested inside other patterns
let tup: (Integer, Real) = (1, 2.0)
let (a: Integer, b) = tup

Sometimes its useful to assert only part of a type. A type "hole", written as _, matches any type. For example, let foo: (_, _) asserts that foo is a tuple with two elements, but does not say what type those elements must be.

Functions

A defining feature of functional programming is that all functions have one parameter. Function types are written using the -> operator, like parameter -> result. When you write a function with multiple parameters, the compiler uses a process called "currying" to transform it into a single parameter function.

example.hc
-- This function with 2 parameters:
let add_2_nums =
  fn a b => a + b
-- Is transformed into this function with 1 parameter:
let add_2_nums: Integer -> Integer -> Integer =
  fn a => (fn b => a + b)
  -- Parenthesis added for clarity,
  -- they are not actually necessary here

The same currying idea in Python looks like this:

example.hc
def add_2_nums(a):
    def add_b(b):
        return a + b
    return add_b

result = add_2_nums(2)(3)
# result == 5

Note: -> is right associative, meaning a -> b -> c is read as a -> (b -> c).

Type Definitions and Aliases

The type keyword is used to define new types and type aliases. A type alias is just a new name for an existing type. An alias is interchangable with the type it refers to - they are both considered the same type.

example.hc
-- The `~` operator makes `Point` a type alias
type ~Point = (Integer, Integer)
let p1: Point = (1, 2)
let p2: (Integer, Integer) = p1
-- This is ok, because p1 and p2 are the same type

Type definitions work differently - a defined type is only equal to itself. Defined types create a "constructor" function with the same name - this is the only way to create a value of that type.

example.hc
type Point = (Integer, Integer)
let p1: Point = (1, 2)
-- Error: (Integer, Integer) != Point
-- To fix this, we must use the constructor for `Point`:
let p1: Point = Point (1, 2)

-- Type equality for definitions is very strict:
type Point2 = Point
let p2 = Point2 (3, 4)
-- Error: Expected an argument with type `Point`,
-- but found `(Integer, Integer)`
let p2 = Point2 (Point (3, 4))
let p3: Point = p2
-- Error: Point2 != Point

Structures

Structures work exactly how you would expect:

example.hc
-- `Dog` is a structure type
type Dog = {
  owner: String,
  name: String,
  breed: String,
}

-- Fields can be assigned out of order
let my_dog = Dog { owner: "Logan", name: "Scruffy" }

-- Fields are accessed using `.`
do println my_dog.owner

However, they come with a few nuances. The compiler sometimes has trouble guessing the type of a structure. Take this code for example:

example.hc
let get_owner = fn dog => dog.owner

It may seem obvious that get_owner has the type Dog -> String. However, the compiler will produce an error here that it cannot determine the type of dog. The only thing the compiler knows about dog is that it has a field called .owner. Imagine if we defined another structure somewhere called Cat:

example.hc
type Cat = {
  owner: String,
  name: String,
  color: String,
}

Now there are two different structures with a .owner field. When a type is ambiguous, the compiler refuses to guess - you must provide clarification. There are a few different ways to do that:

example.hc
-- This works
let get_owner: Dog -> String = fn dog => dog.owner
-- As does this
let get_owner = fn (dog: Dog) => dog.owner
-- You can even use the constructor to hint at what the type should be
let get_owner = fn dog => (Dog dog).owner

The compiler can often find the correct type from context clues, so in practice type assertions are rarely needed.

example.hc
do get_owner { owner = "Logan", name = "Scruffy" }
  |> println
-- No constructor or assertion necessary,
-- we already know the argument to `get_owner` is `Dog`,
-- so we can infer the structure's type.

A structure can "inherit" properties from another structure. This is not comparable to inheritance in OOP languages, it just inlines all the fields of one structure into another's definition. Just because a structure inherits properties from another does not make it a subtype - the two types remain completely different.

example.hc
type Position = { x: Real, y: Real } 
type Circle = {
  radius: Real,
  ..Position
}
-- `Circle` has the fields:
-- radius: Real,
-- x: Real,
-- y: Real

type PhysicsBody {
  ..Circle,
  weight: Real,
}
-- `PhysicsBody` has the fields:
-- radius: Real,
-- x: Real,
-- y: Real
-- weight: Real

Sum types

A sum type can be thought of like an enum

example.hc
type Class =
  | Ranger
  | Fighter
  | Wizard

type Weapon =
  | Bow
  | Sword
  | Staff

-- `Class` and `Weapon` are sum types with three variants each
  
-- Variants can be used in match expressions, and as constant values
let get_weapon: Class -> Weapon = fn
  | Ranger => Bow
  | Fighter => Sword
  | Wizard => Staff

What makes sum types special is that they can also have data attached to each variant. Rather than have one constructor, each variant of a sum type gets its own constructor. If a variant has no data attached to it, a constant value is generated instead of a constructor function.

example.hc
type HttpResponse =
  | Ok String
  | Redirect String
  | Error Integer

let get_status_code: HttpResponse -> Integer = fn
  | Ok _ => 200
  | Redirect _ => 302
  | Error code => code

let response_a = Ok "<h1> Hello, world! </h1>"
let response_b = Redirect "https://google.com/"
let response_c = Error 500

Sum types solve a similar problem to inheritance in OOP languages. Each response variant shares the same type (HttpResponse), but contains different kinds of data. This is similar to subtype inheritance, except that sum types cannot be extended. No library that imports this code can define a new kind of HttpResonse.