Efene Quick Introduction for the Busy/Lazy Programmer

This is a quick and concise introduction to the efene programming language intended for someone who already programs in some programming language, it will only cover syntax and data types so you can familiarize with the language in a short read and maybe use it as a reference in the future.

Data Types

Do you know JSON?

Yes: great! you know almost all of the data types and their syntax

No: you should click on the above link then...

Let’s see a quick example:

{
    "object?": "it's the thing wrapping this fields, we call it a map",
    "string": "I'm a string!",
    "integer": 42,
    "float": 3.14,
    "bool": true,
    "null": nil,
    "empty list": [],
    "list": [1, 2, true, false, ["nested!"]],
    "other object please": {"ok": true}
}

But wait, there is more!

{
    what: '<- that's an atom, like clojure :atom and a ruby :symbol',
    'wait!': 'yes, strings in single quotes are a different type of strings';
    'how?': (binary, strings, are, just, another, type, of, strings),
    'and that?': 'those are tuples, like python tuples'
    'tuple examples': {
        empty: (),
        one_item: (1,),
        two: (1, 2),
        three: (1, 2, 3)
    }
}

lets recap, efene data types are:

  • boolean: true, false
  • null: nil

Those two aren’t actual types, just atoms, but by convention they are used as booleans and null, the undefined atom is also used in many places.

  • atoms: symbolic identifiers that evaluate to themselves. They provide very fast equality tests.
  • integers: 1, 2, 42
  • floats: 1.2, 3.0, 42.7
  • strings: “with double quotes”
  • binary strings: ‘with single quotes’
  • tuples: ((,), (1,), (1, 2, 3))
  • lists: [[], [1], [1, 2, 3]]
  • maps: {empty: {}, one: {one: 1}, more: {one: 1, two: 2}}

Strings vs Binary Strings, the Short Version

Strings are actually lists or integers representing unicode code points.

Binary strings are the binary type with the content being a string encoded in utf8.

Expressions

Yes, expressions, not statements, all the expressions covered below return a value, so you can use expressions on the right side of an assignment to assign (match) the returned value to a variable (well, they don’t vary that much).

Simple Conditions with Guards

A guard is similar to an if in other languages but with some restrictions, since they are used in many places in the language (for example function clauses), the conditions should evaluate fast and be free of side effects, this means the subset of functions you can call in a guard condition is very restricted, you shouldn’t use guards as if, use the match expression instead.

But let’s see some examples:

R1 = when true: ok end
IsInteger = when is_integer(R1): true else: false end
IsInteger1 = when is_integer(R1):
    true
else:
    false
end

IsNumber = when is_integer(A): true
    else is_float(A): true
    else: false
end

As you can see guards are defined starting with the when keyword followed by a condition, followed by a colon and then one or more expressions separated by a new line.

Also you can see that we can assign the result of the when expression to a variable (variables start with an uppercase letter), the result of a guard expression is the result of the last expression of the guard clause that evaluated to true.

More Complex Conditions with Match

As we said, guards are pretty restrictive in what you can put in the condition, that’s why normally we use the match expression or pattern matching, pattern matching is at the core of efene and is used extensively, let’s see some examples:

Result = match some_function(A, B):
    case 42: the_answer
    else: something_else
end

A simple match expression starts with the match keyword followed by an expression that will be evaluated and matched against all case clauses from top to bottom, when one matches the body of that case clause will be evaluated and the last evaluated expression of the body will be the returned value of the match expression.

you can add an else clause at the end to act as a catch all (as in the when expression)

another example:

B = 43
Result = match some_function(true, 14):
    case 42: the_answer
    case 41: almost_there
    case B: missed_it
    case A when is_integer(A): (at_least_an_integer, A)
    case _: something_else
end

This one is a little more complex, we match against a literal value in the first two case clauses, but in the third we match against an bound variable, that is a variable that is bound to a value already, this will match if the result of evaluating some_function(true, 14) returns 43, since B is bound to that value.

The fourth case clause matches against an unbound variable, that is a variable that doesn’t have a value yet, but then it has a guard expression that checks that A is an integer, this case clause will only match if A is an integer, and the A variable will be bound in the case clause body to the result of calling some_function, so we can use it for example to return the value inside a tuple.

The last case clause is another way of writing the else clause from the first example, in this case we match against the special variable _, this variable in efene is used to signify that we are not interested in that value and will match against anything, even if used more than once in the same scope.

Normal variables once they are matched they will only match against the same value, if they are matched against something else they will throw a bad match error, but the _ variable will happily match against anything you throw at it, let’s see an example:

fn my_xor case V1, V2:

  R1 = match (V1, V2):
    case A, A: false
    case _, _: true
  end

  R2 = match V1, V2:
    case B, B: false
    case _, _: true
  end

  T = (V1, V2)

  R3 = match T:
    case C, C: false
    case _, _: true
  end

  R4 = match T:
    case D, D: false
    else: true
  end

  R5 = match T:
    case (E, E): false
    case _: true
  end

  (R1, R2, R3, R4, R5)
end

Here we define a function (new stuff!) that has only one case clause that receives two arguments (V1 and V2) and in the body it does the same thing 5 times in slightly different ways.

Notice that the first case clause in the 5 match expressions will match when V1 and V2 have the same value, since A, B, C, D and E are unbound when evaluating the first argument of the case clause they are bound to the value passed in. Since then they are bound for the rest of the current case clause evaluation, this means that when matching V2 to A (and B, C etc.) since A is already bound to V1, it will only match if V1 is equal to V2.

The second case clause in the first 3 match expressions could be replaced with an else as shown in the fourth or with a simpler case that matches just one _ the difference between the first 3 and the last 2 is that the last two would even match something that is not a two item tuple, but since here we are in control of the expression we are matching against it won’t make a difference.

You may already have noticed that if you write more than one item separated by coma in the match condition or in case clauses they are treated as tuples, this is to have a more terse syntax with something that is really common when writing efene.

When only one item is available in the match condition or in case clauses it’s evaluated as is, it could be a tuple itself or something else, this means you can write tuples surrounded with parenthesis if you prefer but we recommend the cleaner version without them. Notice that the last match has the tuple wrapped in parenthesis (E, E).

Functions

Yes, I wrote a function there without introducing it, I also said “only one case clause”, let’s rewrite the previous code as a function:

fn first_example
  case 42: the_answer
  else: something_else
end

fn second_example
  case 42: the_answer
  case 41: almost_there
  case B: missed_it
  case A when is_integer(A): (at_least_an_integer, A)
  case _: something_else
end

fn my_xor case
  case A, A: false
  case _, _: true
end

That should require little explanation, we define a top level function starting with the fn keyword followed by an atom that will be the name of the function, then one or more case clauses, in the case of functions all case clauses must have the same number of arguments and that defines what we call the arity of the function, in human words, the number of arguments it expects, we can have two or more functions with the same name but different arity.

to call the functions:

first_example(42)
second_example(43)

my_xor(true, false)
my_xor(true, true)

Notice also that we use guard clauses like in the match expression and that the case clauses are exactly the same as match expressions, you should start noticing this recurring pattern in the following sections.

Anonymous Functions

The previous section covered top level (module level) functions, but what if we want to create a temporary function inside a function?

fn build_functions case:
    FirstExample = fn
      case 42: the_answer
      else: something_else
    end

    SecondExample = fn
      case 42: the_answer
      case 41: almost_there
      case B: missed_it
      case A when is_integer(A): (at_least_an_integer, A)
      case _: something_else
    end

    MyXor = fn
      case A, A: false
      case _, _: true
    end

    (FirstExample, SecondExample, MyXor)
end

Here we define a top level function called build_functions with arity 0 that when called will return a 3 item tuple with 3 functions that are identical to the ones defined in the previous section, the only difference is that anonymous functions (as their name implies) don’t carry a name after the fn keyword, other than that they are the same as top level functions.

let’s now use them

(Example1, Example2, Xor) = build_functions()

Example1(42)
Example2(43)

Xor(true, false)
Xor(true, true)

I think it doesn’t require further explanations, after all this is the quick introduction :)

Named Anonymous Functions (WAT)

With a top level function you can call the function from one of the case clauses to do recursion, but what happens if you want to do the same with an anonymous function?

Well you do something like this:

Factorial = fn Fact
    case 0: 1
    case N: N * Fact(N - 1)
end

The name Fact is only visible inside the function’s case clauses and is only used for recursion, you have to assign it to something to use it.

Holding a Reference to a Top Level Function

We saw that we can assign an anonymous function to a variable, but what if we want to do the same for a top level function or a function in another module?

We just write the fn keyword followed by the name of the function and optionally the module if it’s in another module followed by a colon and the function’s arity.

CR1 = fn a:0
CR3 = fn a.b:2
CR4 = fn a.B:3
CR5 = fn A.b:4
CR6 = fn A.B:5

Now you have a reference to the function, this is also useful to pass a top level function to a higher order function.

Handling Exceptions

Yes, efene supports exceptions and has the familiar try/catch/after expression to handle them, let’s jump straight to it:

R1 = try
  1/0
after
  ok
end

R2 = try
  1/0
catch
  case error, badarith: ok
end

R3 = try
  1/0
catch
  case error, badarith: ok
after
  ok
end

R4 = try
  1/0
catch
  case throw, T1: T1
  case Throw: Throw
  case error, E1: E1
  case exit, X1: X1
  case A, C: C
  else: iselse
end

As I said earlier and like all other expressions, try/catch/after expression (from now on try expressions) return a value that you can match to something (if you want).

The expression starts with the try keyword followed by one or more expressions in the try body separated by new lines, if the body doesn’t throw an exception the result of evaluating the last expression of the body will be returned.

If an exception is thrown and no catch section is available the after body will be evaluated and the exception will be re-thrown.

If the catch section is available the thrown value will be matched against each case clause from top to bottom, if a case clause matches the body will be evaluated and the result of the last expression will be returned, if no case clause matches the exception will the re-thrown. If an after section is defined it will be executed before re-throwing.

case clauses in the catch section have one restriction, they can only have one or two arguments, if they have one argument the value will be matched against the exception’s details and the type of the exception is assumed to be throw.

If the case clause has two arguments the first will be matched against the exception type which can be one of throw, error or exit and the second against the exception’s details.

You can also have an else clause as the last one which will match against anything.

Notice that case clauses in the catch section are equal to case clauses everywhere else except for the number of arguments restriction, this mean they can have guard expressions.

Receiving Messages

Efene thanks to it’s runtime provided by the Erlang VM supports message passing, to send messages we use the “bang” operator ! where the left side is where we want to send the message (a process id or an atom) and the right side is the message we want to send, let’s see some examples:

some_registered_process ! 42
Pid ! ok

On the other side we want to receive that message, we do it with the receive expression:

receive
  case 42: the_answer
  case 41: almost_there
  case B: missed_it
  case A when is_integer(A): (at_least_an_integer, A)
  case _: something_else
end

That set of case clauses should be familiar to you, this expression will block waiting for a message sent to the current process and when one is received it will match it against the case clauses, if one matches the result of evaluating its body will be returned (yes, it’s an expression, and yes, case clauses work like everywhere else).

But what happens if no message is sent? well the code above will block forever, we can solve this by adding an after section:

receive
  case A, A: false
  case _, _: true
after 1000:
    timeout
end

The after section starts with the after keyword and is followed by an expression that should evaluate to a number of milliseconds (or the atom infinity), after that time (or never if infinity is passed) the after body will be run and its result returned as the result of the receive expression.

If the timeout value is 0 the after body will be run inmediatly if no message is in the process’ inbox.

For/List Comprehension Expression

For with one generator:

for X in lists.seq(1, 10):
  X + 1
end

For with one generator and one filter:

for X in lists.seq(1, 10); when X % 2 is 0:
  X + 1
end

For with two generators:

for X in lists.seq(1, 10); Y in lists.seq(10, 20):
  (X, Y)
end

The for expression in efene works similarly to a list comprehension in other languages, you can have one or more generator expressions that assign each value of the sequence to a variable and execute the body with the variable bound to that value. You can also have zero or more guards that if evaluated to false will skip the body for that combination of variables, think of them as filters.

The body of the for expression is evaluated and the last expression on each iteration is accumulated in a list and after the for finishes the list is returned.

Begin/End Expression

You need to put more than one expression in a place where only one expression is expected? then begin/end is for you:

Value = begin
    io.format("returning 42")
    42
end

The result of evaluating the last expression will be returned as the result of the begin/end expression

Tagged Values and Expressions

Values and expressions in efene can be tagged, what each tag means is unknown to efene the language, tags are handled by compiler plugins to give them meaning, let’s explore the “official” tags, which are the ones shipped with the standard efene compiler (but you can run efene without them if you wish):

#atom "I'm an atom"
#_ "I'm ignored, useful for comments, yes, we don't have comment syntax"

#_ "The following line evaluated to the integer that represents the character 'A'"
#c "A"

^_ begin
    "this begin/end expression is ignored, useful to comment a whole expression"
    42
end

#_ "The following is a binary pattern"
#b {_: _,
    A: _,
    JustSize: 8,
    JustType: binary,
    E: {},
    _: {size: 8},
    _: {type: float},
    _: {sign: unsigned},
    _: {endianness: big},
    _: {unit: 8},
    B: {size: 8, type: float, sign: signed, endianness: little, unit: 16}}

#_ "Compile time information"
CurrentLine = #i line
CurrentModule = #i module
CurrentModuleStr = #i module_string
CurrentFunction = #i function_name
CurrentFunctionArity = #i function_arity

#_ "Erlang macro expansion (yes we support erlang macros, defined in erlang modules :)"
#m Author
#m LINE
#m PI

#m AUTHOR(bob)
#m Text(1 * 2 + 3)
#m AddPlusOne(2, 3)

#_ "Erlang record support defined using tagged values"
P = #r.person {name: "bob", lastname: "sponge", age:29}
P1 = #r.person P#{age:28}
#r.person {age: Age} = P1
Counter = #r.state.counter State

#_ "Binary Comprehension as an extension too"
^b for A in foo(10):
  A + 1
end

Higher Order Functions

Higher order functions are functions that take functions as parameter and may return functions as result, common higher order functions are map, filter and reduce.

Passing functions around is so common in efene that we provide a nicer syntax for it which also enables some really lightweight dsl construction:

#_ "lists.map:2 takes a function as first argument"
lists.map(List) <<- case X:
  X + 1
end

#_ "mymap takes a function as last argument"
mymap(List) <- case X:
  X + 1
end

The <- operator inserts the anonymous function as the last argument in the function (imagine that <- sends the value to the closest side).

The <<- operator inserts the anonymous function as the first argument in the function (imagine that <<- sends the value to the other side).

Threading Values

efene is a lot about symmetry and consistency, this means if we have arrows pointing in one way surely we should have arrows pointing in the opposite way and doing something similar, right?

Well, we actually do:

IsOdd     = fn case X: X % 2 is 0 end
Increment = fn case X: X + 1 end
MyMap     = fn case List, Fun: lists.map(Fun, List) end

lists.seq(1, 10) ->>
    lists.filter(IsOdd) ->
    MyMap(Increment)

(I define MyMap to reverse the order of the arguments of lists.map so I can use -> in the example)

The ->> operator inserts the value from the left as the last argument in the function on the right (imagine that ->> sends the value to the other side)

The -> operator inserts the value from the left as the first argument in the function on the right (imagine that -> sends the value to the closest side)

This allows you to “pipe” results from one operation to another one without creating temporary variables.

Operators

I won’t introduce operators in detail, you have the language reference for that and you told me you are a programmer and you are busy (or lazy), so, without further ado:

Boolean Operations

Op Description Erlang Equivalent JS Equivalent
or Short Circuit Or orelse ||
and Short Circuit And andalso &&
xor Xor xor No Equivalent
orr Non Short Circuit Or or No Equivalent
andd Non Short Circuit And and No Equivalent

orr and andd are only available for compatibility with erlang and shouldn’t be used.

Comparison Operations

Op Description Erlang Equivalent JS Equivalent
== equal to == == (Not so much coercion)
!= not equal to /= != (Not so much coercion)
< less than < <
<= less than or equal to =< <=
> greater than > >
>= greater than or equal to >= >=
is exactly equal to =:= ===
isnt exactly not equal to =/= !==

The arguments may be of different data types. The following order is defined:

number < atom < reference < fun < port < pid < tuple < list < bit string

Lists are compared element by element.

Tuples are ordered by size, two tuples with the same size are compared element by element.

When comparing an integer to a float, the term with the lesser precision will be converted into the other term’s type, unless the operator is one of is or isnt.

A float is more precise than an integer until all significant figures of the float are to the left of the decimal point.

This happens when the float is larger/smaller than +/-9007199254740992.0. The conversion strategy is changed depending on the size of the float because otherwise comparison of large floats and integers would lose their transitivity.

Concat Operations

Op Description Erlang Equivalent JS Equivalent
++ list concatenation ++ Array.prototype.concat
list subtraction No Equivalent

The list concatenation operator ++ appends its second argument to its first and returns the resulting list.

The list subtraction operator – produces a list which is a copy of the first argument, subjected to the following procedure: for each element in the second argument, the first occurrence of this element (if any) is removed.

Warning

The complexity of A – B is proportional to length(A) * length(B), meaning that it will be very slow if both A and B are long lists.

Arithmetic Operations

Op Description Erlang Equivalent JS Equivalent
+ addition + +
- subtraction - -
* multiplication * *
/ division / /
% remainder rem %
// integer division div No Equivalent

Binary Operations

Op Description Erlang Equivalent JS Equivalent
| binary or bor |
& binary and band &
^ binary xor bxor ^
<< shift left bsl <<
>> shift right bsr >>

Unary Operations

Op Description Erlang Equivalent JS Equivalent
- integer negative - -
not boolean not not !
~ binary not bnot ~

Where to Go from Here

This is a quick introduction of a lot of topics, if you want to learn more you should check the following resources: