Skip to content

Instantly share code, notes, and snippets.

@hzamani
Last active August 20, 2020 07:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hzamani/74396f03d3cb271507f5e111ec92c4d5 to your computer and use it in GitHub Desktop.
Save hzamani/74396f03d3cb271507f5e111ec92c4d5 to your computer and use it in GitHub Desktop.

Syntax

Postprocessing

Semicolons will be added to the token stream, using the following rules:

TODO

Types

Primitive

Primitive types are Integer, Float, String and pairs denoted by (_, _).

nox> 42
  :: Integer
  => 42

nox> 1.2
  :: Float
  => 1.2

nox> "Hello!"
  :: String
  => "Hello!"

nox> (1, "test")
  :: (Integer, String)
  => (1, "test")

Tuples

Tuples are sugar for nested pairs.

TODO: more on this one

nox> (1, 2, 3) == (1, (2, 3))
  :: Boolean
  => True

Pipe

x.negate is syntactic sugar for negate(x)

x.add(y) is syntactic sugar for add(x, y)

Lambdas

lambdas are defined by pattern maching. ~ symbol defines a match clause.

nox> let identity = {~ x => x }
  :: a -> a

nox> let swap = {~ (x, y) => (y, x) }
  :: (a, b) -> (b, a)

nox> let even? = {
   | ~ 1 => False
   | ~ 2 => True
   | ~ n => (n - 1).even?
   | }
  :: Integer -> Boolean

We can match on input by piping to a lamda:

condition.{
~ True  => "Yes!"
~ False => "No"
}

Data Types and pattern matching

type Boolean {
    // these are `Boolean` constructors
    False
    True
}

// `value` is a type parameter
type Option(value) {
    None
    Some(value)
}

define or_else(option: Option(value), default: value) -> value =
    option.{
    ~ None        => default
    ~ Some(value) => value
    }

type Ordering {
    Less
    Equal
    Greater
}
nox> let x = None
  :: Option(a)

nox> let y = Some(10)
  :: Option(Integer)

nox> x.or_else(12)
  :: Integer
  => 12

nox> y.or_else(12)
  :: Integer
  => 10

Recursive Data Types

// this `List` is in types namespace
type List(item) {
    Nil
    // this `List` is in values namespace
    List(item, List(item))
}

Records

type User {
    User[name: String, age: Integer]
}

type Tree(item) {
    Leaf
    Node[left: Tree(item), value: item, right: Tree(item)]
}

define node(value: item) -> Tree(item) =
    Node[left=Leaf, value, right=Leaf]

define depth(tree: Tree(item)) -> Integer =
    tree.{
    ~ Leaf => 0
    ~ Node[left, right] => max(left.depth, right.depth) + 1
    }

Record construction/update

nox> let joe = User[name="joe", age=42]
  :: User

nox> let jack = joe[name="jack"]
  :: User

nox> let tree = Node[
   |     left  = node(1),
   |     value = 2,
   |     right = Node[
   |         left  = node(3),
   |         value = 4,
   |         right = node(5)
   |     ]
   | ]
  :: Node(Integer)

nox> tree.depth
  :: Integer
  => 3

Type Classes

class Equitable(value) {
    define (==)(x: value, y: value) -> Boolean

    // method with default implementation
    define (!=)(x: value, y: value) -> Boolean =
        !(x == y)

    // having explains expected behavior of class methods (sometimes called class laws)
    // by a list functions with Boolean return values
    having {
        // read this as: for all x. expect x == x to be true
        reflexivity(x) =
            x == x
        symmetricity(x, y) =
            x != y || y == x
        transitivity(x, y, z) =
            x != y || y != z || (x == z)
    }
}

// defining class instance
define Equitable(Boolean) {
    // type annotations are optional for defining instane methods
    define (==)(x, y) = x.{
    ~ True  => y
    ~ False => !y
    }
}

// defining class instance with constraints
having Equitable(value)
define Equitable(Option(value)) {
    define (==) = {
    ~ (Some(x), Some(y)) => x == y
    ~ _                  => False
    }
}

// classes can extend others
class Ordered(value) extend Equitable(value) {
    define compare(x: value, y: value) -> Ordering
    
    // classes can provide default implementation for super classes
    define (==)(x, y) = x.compare(y) == Equal
}

class Combinable(value) extends Equitable(value) {
    define combine(x: value, y: value) -> value
    // methods can use class parameters on return value only
    define neutral: value
    
    having {
        associativity(x, y, z) =
            x.combine(y).combine(z) == x.combine(y.combine(z))
        identity(x) =
            x.combine(neutral) == x &&
            neutral.combine(x) == x
    }
}

// using classes as constraints on type variables
having Accumulatable(data) & Combinable(item)
define combine_all(data: data(item)) -> item =
    data.accumulate(neutral, combine)

// classes can have multiple parameters
class Cast(from, to) {
    define cast(value: from) -> to
}

// associated type parameters
class Collection[item](data) {
    define empty: data
    define insert(data: data, item: item) -> data
}

define Collection[item](List(item)) {
    define empty = Nil
    define insert(list, item) = List(item, list)
}

define Collection[(key,value)](HashMap(key, value)) {
    // ...
}

having Accumulatable(data) & Collection[item](collection)
define into(data: data, collection: collection) -> collection =
    data.accumulate(collection, insert)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment