Skip to content

Latest commit

 

History

History
809 lines (574 loc) · 17.6 KB

File metadata and controls

809 lines (574 loc) · 17.6 KB

Odd Syntax

This document describes the Odd syntax. It is a live document and is not a formal specification in any way.


📚 Table of Contents


Comments

You can write a comment by typing two dashes (--) followed by any character, up until a newline:

-- This is a comment.

Comments are legal anywhere whitespace is allowed, meaning pretty much anywhere:

a b c =
  -- Do something with `b` and `c`
  do-something b c;

There are no multiline comments.



Literals

A literal is a lexeme that represents a value literally. Below are all possible literal productions, loosely ordered by simplicity.


Booleans

Odd features two values that represent truth and falsehood: true and false, respectively.

infinity > 1 -- true
1 == ''1''-- false

Nothing

It's really useful to be able to say something holds no value, instead of crashing and burning. There's a special literal for this case: nothing. It behaves somewhat like null would in other c-like languages.

ℹ️ It behaves only somehwat like it. The billion dollar mistake is not repeated. Only nothing can be nothing, no other value can inhabit its type.

Nothing is equal to nothing, except itself:

nothing == nothing -- true
1 == nothing -- false

ℹ️ When the type system is implemented, union types will allow you to express optionality by unioning the types: x : Number | Nothing.

Numbers

A number is any string of consecutive digits:

1;
10;
123456789;

Legibility

To increase the legibility of a number, you can insert a comma (,) anywhere between digits:

1000000;
-- is the same as
1,000,000;

Decimal

Decimal numerals are denoted with a full stop (.) to separate the integral and decimal parts:

1.5;
372.8738;

The integral part is optional:

0.123456789;
-- is the same as
.123456789;

You cannot insert commas after the full stop:

1,000.03534 -- ok!
1.033,534 -- ERROR!

Exponent

A number can contain an exponent, delimited by the letter e (case-insensitive):

1e6;
-- is the same as
1E6;
-- is the same as
1,000,000;

The exponent is assumed to be positive, but can be explicitly marked as such:

1e6;
-- is the same as
1e+6;

The exponent can also be negative:

1e-6;
-- is the same as
0.000001;
-- is the same as
.000001;

Infinity

There is a special literal in Odd that represents a number greater than any other number, named infinity. This is also the case for -infinity, which is always smaller than any other number.

Of course, infinity isn't a number but let's pretend 😉.


Strings

A string is a sequence of any character (except newlines), contained within two single apostrophes (''):

''This is a string!'';

ℹ️ Multiline strings are a planned feature.

Interpolation

ℹ️ String interpolation is a planned feature.

Common pitfalls in strings

The reason Odd uses this syntax is to avoid common pitfalls of strings in other languages.

Strings often contain quotes themselves, requiring multiple string delimiters, or escaping mechanisms:

// JavaScript
"Daniel's dog is named \"Bert\"";
'Daniel\'s dog is named "Bert"';

In Odd, you don't have to escape any quote, nor do you need to switch delimiters to fit your needs:

''Daniel's dog is named "Bert"'';

The reason we chose to use double apostrophes, is because they are available and easily typeable for most people around the world, unlike alternatives such as the backtick (`).


Names

A name — often called an identifier in other languages — is a case-insensitive sequence of any letter and number, beginning with a letter:

name;
name2;

ℹ️ For those who can read it, it's recognised with the following regex:

/[a-z]\w*(?:-\w+)*/i

To separate words within a name, you can use hyphens (-):

this-is-also-a-name;

The way we separate words in a name is often called kebab case, because of the resemblance to kebab on a skewer.

Odd supports kebab case because it's more legible than camelCase or PascalCase, and requires fewer key presses than snake_case. While camelCasing your variables is still possible, the official Odd styleguide requires kebab-case.

Common pitfalls in names

Because the hyphen character is also used for the minus operator, one might get confused between a-name and an - expression. In practise, people mostly put spaces between terms and operators in their maths, so it's easily distinguishable.

This requirement does prevent some code minification, but it's not desirable to send source files over the wire; compiled programs are preferred.


Lists

In Odd, you can group several values into a single value by wrapping it in a list. A list consists of an opening square bracket ([), followed by zero or more expressions delimited by a comma (,), followed by a closing square bracket (]):

[]; -- empty list

[this, is, a, list, of, names];

[ any-value-is-allowed,
  3456,
  10 * .3,
  ''string'',
  x -> y,
  [ lists-in-lists,
    are-also-allowed ] ];

A list can contain a dangling comma.

To merge two lists literally, you can destructure them:

-- given two lists
a = [1, 2, 3];
b = [4, 5, 6];

-- the destructuring
[ ...a,
  ...b ]

-- is equal to
[1, 2, 3, 4, 5, 6];

Records

In Odd, you can categorise several values into a single value by wrapping it in a record. A record consists of an opening accolade ({), followed by zero or more fields delimited by a comma (,), followed by a closing accolade (}).

Fields are optional:

{}; -- empty record

Fields have several forms. A field can be named with a name, followed by an equals sign (=) and then an expression:

{ name = value,
  another-name = another-value };

A record can contain a dangling comma:

{
  a = b,
  c = d, -- <-
};

A field can also be a function:

{ mult a b = a * b };

If there exists a variable var in the current scope, a record can be assigned a field with the name var and the value of var:

{ var };
-- is the same as
{ var = var };

ℹ️ Dynamic field names are a planned feature.

To merge two records literally, you can destructure them:

-- given two records
x = {
  a = 1,
  b = 2,
  c = 3,
};
y = {
  d = 4,
  e = 5,
  f = 6,
};

-- the destructuring
destructured = {
  ...x,
  ...y,
};

-- is equal to
handwritten = {
  a = 1,
  b = 2,
  c = 3,
  d = 4,
  e = 5,
  f = 6,
};

-- as proven by
destructured == handwritten -- yields true

Operators

Most non-letter characters are treated as operators. Multiple non-letter characters in a row are recognised as one operator:

multiply = (*);
divide = (/);
fish = (><>);
brainfuck = (+<>-+>-);

ℹ️ For those who can read it, operators are recognised with the following regex:

/[-!@#$%^&*_+=:\|\/\\\.\<\>\?]+/

Operators such as -> and = are special and cannot be overwritten.

The Odd prelude defines often-used operators in global scope.

An operator is placed between two expressions:

a + b;

Precedence

All operators have the same precedence and associativity, even common mathematical ones!. This means all infix expressions are evaluated left-to-right.

1 + 2 * 3; -- Forget what shool has taught you, it's 9
-- because it's the same as
(1 + 2) * 3; -- also 9

This might cause confusion for some at first, but this actually prevents program errors caused by imperfect knowledge of all operators and their respective precedences/associativities.

To motivate this decision, consider the following example. One might write expressions such as:

a @@ b </> c % d;

Can you see — at first glance — in what order this expression is evaluated? Why, yes! Left-to-right, like any other infix expression in Odd. You don't need intimate knowledge of the operators to know how the expression flows.

Mathematical operators hold no special distinction, so they are also stripped of their precedence and associativity rules.

To change the order of operations, you can use parentheses:

a @@ b </> c ## d;
-- is the same as
((a @@ b) </> c) ## d;
-- but not the same as
a @@ (b </> c) ## d;

To improve reading comprehension, dense infix expressions are often already redundantly parenthesised in programs in other languages. All operators having the same precedence and associativity means that Odd "enforces" clarity through parentheses.

Application

Because operators are values, they can also be used in application:

(-) 2 3;
-- is the same as
3 - 2;

Note the order of the operands. This order is more akin to how people think about partial application than if they were reversed:

subtract-one = (-) 1;
subtract-one 3 -- yields 2

Whereas the opposite doesn't make sense unless you use a different function name:

subtract-x-from-1 = (-) 1;
subtract-x-from-1 3 -- would yield -2 if the operands were reversed

An operator literal requires parentheses (()) around it if not in infix notation.

They can be used in assignments too, which we will expand upon later.


Lambdas

In Odd you can write nameless functions that are values themselves. These are often called lambdas, because the idea stems from the lambda calculus, which denotes functions with the lambda character (λ).

Lambdas in Odd are denoted by an arrow (->) between parameters and body:

param -> body;

Functions can have multiple parameters, and so can lambdas:

a b c -> a - b + c;

To prevent the parser getting confused over which values are parameters in an application, you can use parentheses:

-- A lambda with two parameters `a` and `b`
a b -> c;

-- Application of `a` to `b -> c`
a (b -> c);

Assignment

You can assign a value to a name using the equals sign (=):

a = b;

Odd values are immutable:

a = b;
a = c; -- ERROR! Cannot redeclare `a`.

A function can be declared with the same syntax:

multiply a b = a * b;

ℹ️ We will expand upon function syntax later on.



Application

Function application in Odd is done by juxtaposition:

f x;

This syntax allows for more natural notation of data transformation.

This differs from many C-like languages and math notation:

f(x);

One could think of these syntaxes as the same, but without requiring parentheses (the c-like syntax is also legal syntax in Odd).



Pattern matching

Patterns allow you to match data by its value or shape.


Case expressions

With a case expression, you can couple values together based on any pattern.

A case expression consist of the word case, followed by a literal (numbers, strings, names, etc.), or an expression between paretheses (), followed by the word of, followed by one or more cases:

two-is-even = case (is-even 2) of
  true = ''yep'',
  false = ''nope'';

two-is-even; -- ''yep''

A case consists of a pattern, followed by an equals sign (=), followed by an expression.

Here are some valid patterns:

  • Literals, such as
    • Strings
    • Numbers
    • Booleans
    • etc.
case (''a'') of
  1 = 1, -- no match
  true = 2, -- no match
  ''a'' = 3; -- match!
-- final result is 3
  • The success case, which is one or more underscores (_), and always matches.
case (''a'') of
  1 = 1, -- no match
  true = 2, -- no match
  _ = 3; -- match!
-- final result is 3

The order of cases is important. They are evaluated top to bottom / left to right:

case ''a'' of
  1 = 1, -- no match
  true = 2, -- no match
  ''a'' = 3, -- match!
  _ = 4; -- also a match, but will not
         -- be chosen since it comes
         -- after another matching pattern
-- final result is 3

Patterns in declarations

You can also destructure values in a declaration.

For example, you can destructure specific properties from a record parameter as follows:

daniel = {
  name = ''Daniel'',
  age = 20,
  occupation = ''Steward'',
};

get-name { name } = name;

get-name daniel -- ''Daniel''

The same applies to lists:

names = [
  ''Daniel'',
  ''Baernhardt'',
  ''Molly'',
  ''Iseabail'',
];

head [first] = first;

head names -- ''Daniel''

You can destructure specific elements and wrap up the rest into another name:

-- lists
[a, ...b] = [1, 2, 3];
a -- yields 1
b -- yields [2, 3]

-- records
{a, ...b} = {a = 1, x = 2, y = 3};
a -- yields 1
b -- yields {x = 2, y = 3}

Patterns work recursively:

-- lists
[[[x]]] = [[[1]]];
x -- yields 1

-- records
{a={b={c}}} = {a={b={c=1}}};
c -- yields 1

Lambdas

Because function declarations are sugar for writing lambdas, the same pattern matching is possible in lambdas:

people = [
  {
    name = ''Daniel'',
    age = 20,
    occupation = ''Steward'',
  },
  {
    name = ''Bearnhardt'',
    age = 53,
    occupation = ''Medieval Knight'',
  },
  {
    name = ''Molly'',
    age = 41,
    occupation = ''Stewardess'',
  },
  {
    name = ''Iseabail'',
    age = 19,
    occupation = ''Nurse'',
  },
];

-- yields [
--   ''Daniel'',
--   ''Baernhardt'',
--   ''Molly'',
--   ''Iseabail'',
-- ];
names = map ({name} -> name) people;


Modules

Any file can be imported into another file or repl by using the import function:

module = import ''module''

No special syntax for importing, you just assign it to a name.

A module cannot explicitly mark a value to be exported. Any names in the global scope are exported:

-- numbers.odd
a = 1;
b = 3;
-- main.odd
numbers = import ''numbers''; -- { a = 1, b = 3 }
numbers ''a'' + numbers ''b'' -- 4

To import only specific names from a module, you can destructure them:

-- module.odd
a = 1;
b = 2;
c = 3;

-- main.odd
{ a, c } = import ''module'';
a -- 1
b -- Error: "b" is not defined
c -- 3


Functions

A function is written like any other value declaration:

add a b = a + b;

The syntactical difference between a normal value and a function is that a function has multiple patterns on the left side of the equals sign. The first symbol is the name of the function, followed by one or more patterns that are treated as parameters.

This syntax is sugar for assigning a lambda to the leftmost symbol, meaning it is curried by default:

a b c d = 3;
-- is equal to
a = b c d -> 3;
-- which in turn is equal to
a = b -> c -> d -> 3;


Types

ℹ️ Types are currently in an "alpha" phase, meaning they're not guaranteed to work as intended. Please submit your issues when they arise!

Types are written like expressions. The convention is to start types with an uppercase letter, except for type variables, which are serialised as Greek letters:

[1, 2, 3] -- [ 1, 2, 3 ] : List Number
double a = 2 * a -- double : Number -> Number
return x = x -- α -> α

Typeclasses

ℹ️ For a far more complete introduction into typeclasses, please read this article about typeclasses in Haskell.

To support polymorphism in a non-hacky way ("hacky" is commonly referred to as ad-hoc), Odd uses typeclasses. In short, this is a method to allow typing expressions such as 1 == 2.

If there are no typeclasses, the operator == might be typed as follows:

(==) : Number -> Number -> Boolean

So that we can write the expression 1 == 2 to get the value false, as expected.

But then, what about strings, booleans or any other datatype? We can't define the type for == multiple times, so we need another way. For values that can be checked for equality, there is the Eq class, which defines the types for == and != as follows:

class Eq a where
  (==) : a -> a -> Boolean,
  (!=) : a -> a -> Boolean;

To provide an implementation of == or != form any datatype you must create an instance of the desired class for the type(s) you want it to apply to:

instance Eq Number where
  a == b = equal a b,
  a != b = not (equal a b);

Under the hood, this allows Odd to select the correct implementation when you write an expression such as 1 == 2.



🤸 Contribute

Conceptualised and authored by (@maanlamp). Feel free to contribute: create an issue or a pull request.