The Topoi Programming Language

Reasonable, sound, and friendly. Write terser code.

A type-safe functional programming language that aims to be terse, small core functionality and simple, featuring dependent type and type inference.

This project is still WIP implementation of these ideas. If you are interested, please support the project via stars. Thank you for your time and generosity.

Foreword

This document is intended as a reference manual for the Topoi language. It lists the language constructs, and gives their precise syntax and informal semantics. It is by no means a tutorial introduction to the language.

In future we will make a proper landing page for the language. But right now it serves as our internal team's draft.

Notations

The syntax of the languages is given in BNF-like notation.

Where square brackets [...] denote optional components, curly brackets {...} denotes zero, one or several repetitions of the encolesd components.

Curly brackets with a trailing asterisk sign {...}* denote one or several repetitions of the encolosed components.

Parentheses (...) denote grouping of components.

Notation Repetition
{...}? 0 or 1
{...}* 0 or more
{...}+ 1 or more

Identifier and Keywords

identifier           
  = letter (letter | "_" | "'" )* 
  | "_" identifier

qualified_identifier 
  = (identifier ".")+

When referencing something inside a module or a record, or a tuple, we use a qualified name (qualified_identifier).

Comment and Documentation

prefix                 
  = letter (letter | "_" | "'" )* 
  | "_" prefix

comment
  = "--" (char)* newline

doc_comment 
  = "--|" (char)* newline

prefixed_doc_comment
  = "--|" prefix "|" (char)* newline

block_comment 
  = "--[" (char)* "]--"

prefixed_block_comment 
  = "--[" prefix "|" (char)* "]--" 

Content enclosed in documentation comment (doc_comment) and prefixed block comment (prefixed_block_comment) are conversed in AST and will be output or generated as web content.

The prefix is just a name that will let compiler pass to know what generator used to process the content inside the doc comment or block comment. eg.: LaTex, KaTeX, markdown...

Primitive value and types

Full Grammar

program 
  = {definition}+

definition 
  = constant_definition
  | type_definition

constant_definition
  = assignable [":" type] ":=" expression

assignable
  = identifier
  | destructurable

destructurable
  = "(" 
    {identifier [":" type] ["as" assignable]","}* 
    ["..." identifier] 
  ")"

expression
  = function_abstraction
  | function_application
  | identifier
  | record
  | adt_construction

function_abstraction
  = "{" {assignable "->" expression ","}+ "}"

function_application
  = expression "." expression

record
  = "(" {record_body ","}*")"

record_body
  = identifier [":" type] [":=" expression]
  | spread_expression

spread_expression
  = "..." expression

type
  = identifier
  | function_type
  | record_type
  | type_function_application

type_function_application
  = function_application

function_type
  = record_type "->" (record_type|function_type)

record_type
  = "(" {identifier [":" type]}* ")"

type_definition
  =  "type" identifier [":" type ":=" type_body]

type_body
  = {"|" constructor [record_type] ":" type}+

constructor
  = identifier

Expressions

Record

Record expression are key-value pairs. Each record are enclosed by round brackets.

Record Construction

Empty record is denoted as ().

Examples of non-empty records:

(name := "topoi", age := "12")

Each property of a record can so be optionally typed, to ensure that the property will be initiated with the correct type.

(name : String = "topoi", age := "0")

We can also construct nested record:

(
  name := "topoi", 
  friend := (
    name := "haskell", 
    age := "30"
  )
)

Shorthand construction

Suppose we have two variables x and y, to create a record like (x := x, y := y), we can instead write as (x, y). This feature can prevent property mismatch like (x := y, y := y).

Record Type

The type of each record can be inferred by the Topoi type system, for example (name := "topoi", age := "12") has the type of (name: String, age: String)

Record destructuring

The property of each record can be destructured, and be populated in the current environment.

(name, age) := (name := "topoi", age := "0")
-- `name` and `age` will be exposed to the current environment as named variables

Note that not all of the properties need to be destructured.

(name) := (name := "topoi", age := "0")

Also, the destructuring order does not matter:

(age, name) := (name := "topoi", age := "0")

Besides that, we can also re-alias the property name using they keyword as.

(name as myName, age) = (name := "topoi", age := 1)
-- `myName` and `age` is exposed, but `name` is not

Accessing properties

We can do it by using the dot notation.

record.propertyName

For example:

me := (name := "john", age := 2)

myName = me.name

Mutating properties

In Topoi, properties mutation is not allowed, however we can use the spread notation with the triple dot operator.

For example,

me := (name := "john", age := 2)

newMe := (...me, age := 3)

Basically it's the same as JavaScript spread operation.

We can also combine the properties of multiple records into one as such:

me := (name := "john", age := 2)
otherDetail := (address := "sea")

fullMe = (...me, ...otherDetail)

Function

Constructing function

We can construct an unnamed function using the {arguments -> body} notation.

However, note that the function arguments must be of record type, and the return type must also be record type or function type.

For example:

add := {
  (value: Int, by: Int) -> (
    result := 0
  )
}

Function type

Each function has a type, for example, the add function above has the type of:

(value: Int, by: Int) -> (result: Int)

Because of that, we can also define the add function as such:

add 
  : (value: Int, by: Int) -> (result: Int)
  = {
    (value, by) -> (result := 0)
  }

Using function

In Topoi, we can use (a.k.a. apply) a function using the following notation (note that this is the only allowed notation):

arg1.func(arg2 := value1, arg3 := value2, ...)

Note that the first argument can ignore the property name, and the order is not important.

For example, the following function usage are equivalent:

1.add(by := 2)
2.add(value := 1)
(value:=1, by:=2).add

We can also chain function call:

1.add(by:= 2).add(by:= 3) 

Because of the dot notation, we can also directly apply an unnamed function (a.k.a. lambda) to an expression.

(x:=1, y:=2).{
  (x, y) -> x.add(by:=y)
}

Pattern Matching (not finalized)

We can pattern match a value using the equal operator.

For example:

divide : (value: Int, by: Int) -> (result: Result.where(ok:= Int, fail:= String)) := {
  (by = 0) -> Result.fail("Cannot divide by 0"),
  (value, by) -> (
    result := -- something
  )
}

Another example:

type Shape := 
  | Circle (radius: Float)
  | Rectangle (height: Float, width: Float)
  | None

area: (shape: Shape) -> (result: Float) := {
  (shape = Circle(radius)) -> 
    radius.square.multiple(by:= pi),

  (shape = Rectangle(height, width)) ->
    height.multiple(by:= width),

  (shape = None) ->
    0
}