Learn Cognate

A brief introduction to the language

Install

First install CognaC the Cognate Compiler from here using the provided instructions. Currently CognaC will run on recent Linux or Mac systems. Windows users can install it onto the Windows Subsystem for Linux – native Windows support is planned.

Invoking CognaC is simple. If you have a file named foo.cog containing a Cognate program, it can be compiled into an executable named foo with the following command.

cognac foo.cog

To compile a debug executable, which will run slower but give a nice backtrace if there’s an error, you can use the -debug flag.

Alternatively, you can use the interactive web playground here which runs Cognate programs client-side in the browser. It also has intelligent syntax highlighting, code folding, and error reporting in the editor

First Programs

Print "Hello world!";

Fairly simple right? This example calls the Print function with one parameter – the string "Hello world!". Now let’s do another simple example, adding two numbers.

Print + 2 3;

Wait what? That isn’t how maths works!

Cognate doesn’t care how maths works. Where in most programming languages, + would be an operator and would be used in the infix position, in Cognate + is a function. Like Print, the + function is called by being written before its arguments like shown above.

Print - 10 36; ~~ Subtracts 10 from 36

Another thing to note is that the order of parameters for - and / are backwards. The reason for this will become clear later. Also note that line comments are started with ~~.

The Stack

Let’s have a more complex example, this subtracts 12 from 15 and then multiplies the result by 2.

Print * 2 - 12 15;

By now you may have realised that Cognate is evaluating our programs backwards – right to left. The subtraction is being performed before the multiplication above. This is being done using a stack, as explained here.

Cognate comes with functions to manipulate the stack. The simplest of these is Twin, which takes the top element from the stack, and puts it back on again – twice. The below snippet uses Twin to square a number by multiplying it by itself.

Print * Twin 8;

Now we don’t want to write this every time we square a number right? So let’s create a Square function to do this for us.

First Functions

Functions in Cognate are defined using Def and the name of the function. The function body is put in brackets after this.

Def Square (* Twin);
Print Square 8;

This is great, but it certainly could flow better. Cognate ignores words starting with lowercase letters, allowing them to be used as comments. This lets us write:

Print the Square of 8;

In this example the readability isn’t really improved much, but in more complex programs this ‘informal syntax’ can be invaluable.

By now you’ve probably noticed the semicolons. These delimit statements so that Cognate knows what order to evaluate functions in (remember that these are executed backwards). Definitions should also be terminated with semicolons. The stack persists between statements, letting us do things like this.

8 ; Square ; Print ;

or this

Square 8 ; Print ;

or this

8 ; Print the Square ;

This flexible syntax allows Cognate programs to flow as if written in sentences. It also means that much of the time variables are not even needed. Of course, Cognate does have variables.

Variables

Variables are defined in a fairly similar manner to functions using Let and their name. This takes a value from the stack, binding it to this name. Variables are referenced with the same syntax in which functions are called.

Let X be 4;
Print X;

We can use variables to define functions that take named parameters – here’s an alternate version of the Square function.

Def Square as (
	Let X;
	* X X
);

Notice that the last statement before a closing bracket does not need to be terminated with a semicolon. Also there is no return statement, since values are implicitly returned on the stack. This means we can actually define functions that return multiple values, such as the Twin function we saw earlier.

Def Twin as (
	Let X;
	X X
);

Control Flow

Like most programming languages, Cognate has if statements and loops and all that jazz. The simplest form of control flow is When, which simply executes a block of code if given True or not if False.

When == 1 1 (
   Print "Who'd have guessed?"
);

There is also Unless that evaluates the block if given False. While takes a condition in brackets and evaluates the block of code until the condition is False. Until, you guessed it, is the opposite of While and runs until the condition is True.

While (True) (
   Print "This may well print forever"
);

What’s with the second set of brackets? When doesn’t have them, so why should While? This is because brackets denote blocks! These prevent code being instantly evaluated and instead push a reference onto the stack. Blocks also control variable scopes. We can evaluate a block using the Do function – which is how our control flow functions are implemented.

Do (
   Print "Hello from inside a block!"
);

This explains the syntax for functions: Def simply binds a block to a name, much like Let. Blocks can be passed around the program like any other value – even if they reference variables that go out of scope.

Now that this (hopefully) makes some sense, we can finally introduce the If statement! If is a function that takes three parameters. The first is a boolean, if this is True then the second argument is returned. If not, the third argument is returned. We can chain Ifs together to have more complex control flow.

Print
	If == X 1 then "foo"
	If == X 2 then "bar"
	          else "baz";

We can combine this with Do to have conditional code execution.

Do
	If == X 1 then ( Print "foo" )
	If == X 2 then ( Print "bar" )
	          else ( Print "baz" );

Now, let’s use our knowledge of blocks to define our own control flow function Thrice, which should evaluate a block three times in a row. This demonstrates a different use of Def in which we bind a block from the stack.

Def Thrice as (
	Def F;
	F F F
);

Thrice (
   Print "hip hip hooray!"
);

A more general version of this function, Times can be defined using recursion.

Def Times (
	Let N number of repetitions;
	Def F function to repeat;
	Unless Zero? N ( F ; Times - 1 N (F) );
);

Times 5 (
	Print "wow!";
);

Now you may see a small problem with this. If the user calls Times with a non-integer parameter it will loop forever – that won’t do at all! We can use the Of function here – it checks a value against a predicate (in this case Integer?) and throws an error if it fails.

Def Times (
	Let N be Of (Integer?) number of repetitions;
	Def F function to repeat;
	Unless Zero? N ( F ; Times - 1 N (F) );
);

We could also use Of (Block?) for F but we’ll already get a type error if we use Def to bind anything that isn’t a block, so there is no point.

Lists

In Cognate, lists are generated using – you guessed it – a function. The List function takes a block as a parameter. It evaluates this block in a new empty stack and then returns that stack as a list.

Print List (1 2 3 4 5);

This allows Cognate’s list creation to be much more flexible than other languages – for example what if we wanted a list of 100 ones?

Print List ( Times 100 (1) );

The three most fundamental list functions are Push, First, and Rest.

The Push function takes a value and a list as parameters, and returns a new list list with the value as it’s first element and the list parameter providing following elements. First simply returns the first element of a list. Rest returns a list without its first element.

Let L be Push "foo" to List ( "bar" "baz" );
Print First element of L;
Print Rest of L;

There’s also an Index function which takes an integer parameter and a list, returning the corresponding list index (indexed from zero of course).

Range creates a list of numbers from a starting (inclusive) and an ending (exclusive) number. For is a higher order function that applied an operation to each element of a list – it is the loop for iterating over lists.

Def Square as (* Twin);
For each in Range 1 to 20 (Print the Square);

Map is like For but the result of the computation is stored in a new list.

Let Evens be Map (* 2) over Range 1 to 50;
Print Evens;

Filter applies a function to each element of a list also. This function should return a boolean – if this is False then the function is removed from the returned list.

Let Even? be (Zero? Modulo 2);
Let Evens be Filter (Even?) over Range 1 to 100;
Print Evens;

The functional programmers reading this are likely expecting a Fold or Reduce function next – which applies an operation to a list with an accumulator. However Cognate needs no fold function, as For can store intermediary values on the stack, acting like a fold.

Def Factorial as (
   Let N be Of (Integer?);
   For each in Range 1 to N (*) from 1;
);

Print Factorial 10;

For the sake of convenience Cognate does provide Fold, which has the order of parameters swapped:

Def Factorial as (
   Let N be Of (Integer?);
   Fold (*) from 1 over Range 1 to N;
);

Print Factorial 10;

Boxes

While storing state between loop iterations is very useful, in some cases you just need mutable variables. Cognate’s box type implements a reference and generalises mutation.

The Box function takes a value and places it in a box. Unbox returns the item stored in a box. Set takes a box and a value as parameters and mutates the box to hold the value – this affects all references to it.

Let X be Box 1;
Print Unbox X; ~~ prints 1

Set X to 2;
Print Unbox X; ~~ prints 2

Boxes aren’t limited to mutating variables, they can be used for any value.

Let L be Map (Box) over Range 1 to 10;
Print L;

For each in L (
   Let X;
   Set X to * 2 Unbox X; ~~ double the element in place
);
Print L;

While boxes may not seem as ergonomic as mutation in other languages, they’re both more flexible than mutable variables and more predictable than implicit references. We can also easily extend mutation, like this:

Def Box-list as ( Map (Box) );

Def Inplace-map as (
	Def F;
	Let L;
	For each in L ( Let X ; Set X to F Unbox X );
);

Let L be Box-list Range 1 to 10;
Print L;

Inplace-map (* 2) over L;
Print L;

Tables

The table type provides an efficient, immutable, unordered mapping between keys and values, which can be used to implement many more complex data structures. Table creates a table in the same manner in which List creates a list, though taking key-value pairs. . is used to extract the value corresponding to a key.

Let T be Table (
	"foo" is 1;
	"bar" is Range 2 to 10;
	12 is 13; ~~ Keys can be of any type except box or block
);

Print . "foo" T;
Print . "bar" T;

Insert returns the table with an extra key-value pair, Remove returns the table without a specified key, and Has checks whether a key is in the table.

Def Remove-baz (
	Remove "baz"
	from Of (Has "baz")
	     Of (Table?);
);

Insert "baz" is Range 11 to 100 into T;
Print Twin;
Remove-baz;
Print;

Keys and Values returns lists of the keys and values respectively in a table, in no particular order. Tables are implemented as self-balancing binary trees, and are optimised for fast immutable insertions.

Symbols

Typically, you’d want to use a symbol as a key for your table. Symbols will be familiar to Lisp programmers and can be considered as either a limited string or an unlimited enum, depending how you like to think.

\foo ~~ This is a symbol

Symbols can’t be modified in any way, but can be compared very efficiently (which is why they’re great keys for tables) and put into any data structure.

Begin

Earlier when we defined loops, you may have noticed something missing – the break statement. Being a functional language, Cognate discourages this sort of control flow, but sometimes you’ve just gotta get out of a block early. Introducing Begin – this function takes a block parameter and evaluates it, passing it another block on the stack. Evaluating that block will jump you out of the original block. Confused? Here’s an example.

~~ Let's print the numbers up to 100 in an unnecessarily complicated manner.
Let I be 1;

Begin (
	Def Break;
	While (True) (
		Print Unbox I;
		Set I to + 1 of Unbox I;
		When == 101 Unbox I ( Break out of the begin );
	)
);

This essentially allows any control flow to have a break statement, not just loops. Begin can also be used to implement a return statement to break out of a function early.

~~ Inefficiently decrements a number 100 times
~~ If it reaches zero, returns zero instead.
Def F (
	Begin (
		Def Return;
		Let X be Box Of (Integer?);

		Times 100 (
			Set X to - 1 Unbox X;
			When Zero? Unbox X ( Return 0 );
		);
		Return Unbox X;
	)
);

An advantage of Begin over traditional programming languages’ break and return statements is that it gives fine-grained control over which block you break out of, since nested Begin statements can have their exit blocks bound to different names.

End

Nope, there isn’t an End function. This is just the end of the tutorial.

Check out the prelude to continue exploring.