The Clara programming language
Clara is a statically typed, general purpose, natively compiled language in the spirit of Go which aims to be fun & clear.
For an idea of where it might be going, see the What’s Next section.
To get Clara running on your machine you’ll need the following available on your $PATH
:
$GOPATH/bin
Then if you are on Linux or Mac simply run the following:
git clone https://github.com/g-dx/clara/
cd clara
./build-and-run hello.clara
This will build the compiler, run all tests, prepare the standard library, compile the ./install/examples/hello.clara program and run it. All being well you should see the familiar “Hello World!” message in your console.
The following diagram shows how the Clara compiler & GCC/Clang work together to produce platform native binaries.
Currently only OSX & Linux are supported but Windows is hopefully coming soon!
Clara’s heritage is mixed but can largely be traced to the following languages:
null
value from the start.As usual we start with the obligatory hello world program:
fn main() {
"Hello World!".println()
}
Clara contains the following built-in types which will be familiar to most users.
bool
- with two possible literal values, true
& false
int
- a signed, 63-bit integer defined using decimal or hexadecimal notationstring
- a block of memory holding a contiguous series of bytes which represent textbytes
- a block of memory holding a contiguous series of bytes.
// Bool
true
false
// Integer
100
-2
0xFF
// String
"Hello!"
// Bytes
Bytes(10)
Clara contains two statements for working with variables; declaration & assignment :=
&
assignment =
// Declare a new variable 'x' in the current lexical scope and
// assign it the integer value 100
x := 100
// Assign variable 'x' the integer value 1000
x = 1000
// Compiler error as 'x' cannot be redeclared!
x := 10000
In Clara there is no way to construct an uninitialised variable.
Clara supports arrays of values. These are laid out contiguously in memory. They may be constructed two ways.
array
function orArrays are not permitted to contain null
or undefined
values. Either a default value
must be supplied which is written into all slots or the returned array is filled with empty
optionals.
// Array of size 10 with '0xFF' written to all slots
allZeros := array(10, 0xFF)
allZeros[0].printHex() // OxFF
allZeros[1].printHex() // OxFF
// ... etc ...
// Array of size 10 with None«string» written to all slots
optionals := array«string»(10)
optionals[0] = Some("Value")
optionals[1] = Some("Another Value")
// Array literals
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].foreach(print) // Prints 0 1 2 ... etc
[false, true, true]
strings := ["hello", "world", "!"]
Clara supports a 2 control flow constructs; while
& for
.
vals := [1, 2, 3, 4, 5, 6, 7, 8, 9]
i := 0
while i < vals.length {
vals[i].println()
i = i + 1
}
A safer way to write the above loop is to use the for
statement:
for val in [1, 2, 3, 4, 5, 6, 7, 8, 9] {
val.println()
}
(The compiler actually lowers all for
statements to while
statements)
Another way to write the above loop is using the for .. in
statement which supports integer ranges
for val in 0 .. 10 {
val.println()
}
When the range is increasing the beginning is inclusive and the end is exclusive. These are flipped when the range is decreasing.
Clara maintains a clean separation between code & data. This means there is no way to couple a function to any data other than its inputs or output.
Function declarations are fairly straightforward. The following computes the fibonacci sequence:
fn fib(n: int) int {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
The fib
function accepts a single parameter n
of type int
& returns an int
.
Functions which do not return anything are said to return nothing
and declaring the return type is optional:
fn hello(msg: string) {
"Hello message is: ".append(msg).println()
}
Functions which return the result of a single expression may use the function expression syntax and omit the return keyword & curly braces:
fn isEven(x: int) bool = x.mod(2) == 0
Functions can be invoked in the normal way by passing all required arguments but may also be invoked using “dot selection” notation. The following example illustrates this:
fn main() {
add(2, 3)
2.add(3) // Equivalent to the line above
}
fn add(x: int, y: int) int = x + y
Here it important to note that the second add
function call is simply “syntactic sugar” for the
first add
call. These are functionally equivalent. This allows for easy function chaining & also “extending” foreign
types by simply writing a new function which takes that type as its first argument:
fn main() {
"Clara!".emphasize().println()
}
fn emphasize(s: string) string = "** ".append(s).append(" **")
Functions in Clara are also said to be “first class” in that they can be passed as arguments to other functions, returned as values from functions & assigned to local variables or stored in data structures.
fn main() {
msg := "Down with this sort of thing. Careful now!"
style := italic
style.applyTo(msg).println()
style = [bold, italic].compose()
style.applyTo(msg).println()
}
fn applyTo(f: fn(string) string, s: string) string = f(s)
fn bold(s: string) string = "**".append(s).append("**")
fn italic(s: string) string = "_".append(s).append("_")
fn compose(styles: []fn(string) string) fn(string) string {
return fn(msg: string) string {
for style in styles {
msg = msg.style()
}
return msg
}
}
Clara also supports both anonymous functions and closures:
fn main() {
square := fn(x: int) int = x * x
"6² = ".append(square(6).toString()).println()
next := intStream(0)
next().println() // 1
next().println() // 2
next().println() // 3
next().println() // 4 ... etc
}
fn intStream(x: int) fn() int {
return fn() int {
x = x + 1
return x
}
}
Structs represent a composite data type over other types, including other structs. Here is an example of a employee
struct:
struct employee {
name: string
age: int
department: string
active: bool
}
fn main() {
e := Employee("Clark Kent", 35, "Journalism", true)
}
The compiler automatically generates a constructor function for all struct
s using a capitalised version
of the struct name. This has the requirement that all struct
names must begin with a lowercase letter.
Enums, also called tagged unions, variants or sum types, are a data type which may hold one of a fixed list of possible values.
enum food {
Pizza(kind: string, inches: int)
Soup(vegetables: []string, containsMeat: bool)
}
fn main() {
f := Pizza("Pepperoni", "12")
f := Soup(["pea", "mint", "courgette"], false)
}
// TODO: Expand this further
Structs, enums & functions also support parameter types or parameterisation of the type of their parameters or fields.
struct box«T» {
val: T
}
fn main() {
intBox := Box(1) // box«int»
boolBox := Box(false) // box«bool»
intBox = boolBox // Compiler error!
}
// TODO: Expand this further
Clara contains a small standard library with support for the following functional types:
The standard operators such as map
, filter
, then
, peek
, etc are supported.
Clara contains a a precise, non-moving, incremental mark & sweep garbage collector built on top of the Tag-Free Garbage Collection scheme. This approach is interesting in that the pointer information gathered during compilation is used to generate program specific garbage collection routines such that no interpretation of memory locations at runtime is required. In this sense every program contains a completely bespoke garbage collector.
For a thorough explanation see the linked paper.
The following is a list, in no particular order, of features which are slated for inclusion in the language.