-
Notifications
You must be signed in to change notification settings - Fork 32
LanguageReference
all the descriptions here apply to both efene and ifene.
both languages share almost all the syntax except that in efene blocks are delimited with curly brackets (“{” to open a block and “}” to close it) and in ifene (indented efene) the indentation level defines a new block, that is, if a block is more indented than the previous statement, then it’s a block inside that previous statement.
Indentation in ifene is made using spaces, not tabs, so configure your text editor to replace tabs with spaces or you will get syntax errors. 4 spaces is the recommended number of spaces to represent a level of indentation.
the examples will show the syntax for both languages, if needed the syntax for both languages will be shown.
Variables start with uppercase and can’t be set twice (single assignment)
A = 2
to change its value assign to a new variable
B = A + 1
each expressions ends with a new line, you can make multi line statements by breaking the line after an operator.
E = 12 +
5 * 2
List = [1, 2,
3, 4]
arithmetic expressions are like any programming language
A = (2 + 3) * (6 / (B + 1)) - 5 % 2
logic expressions are like python
(true or false) xor false and not true
binary operations are like C/C++/Java and similar languages
Bin = (2 << 5) | (255 & ~0)
different ways to express numbers (decimal, hexadecimal, octal, binary)
D = 12 + 0xf - 0o10 + 0b1101
strings
S = "Hello"
S1 = "World"
S2 = S ++ " " ++ S1
the basic types supported are integers, floats, booleans, strings which you saw above and lists, tuples, binaries and atoms.
List = [1, 2, 3, 4]
lists can contain any type inside (even other lists)
List2 = [1, 2.0, false, ["another list"]]
tuples are like lists but once you define them you can’t modify them
Tuple = (1, 2, 3, 4)
and also can contain anything inside them
Tuple2 = (1, 2.0, false, ["a list", ("another", "tuple")])
binaries are a type that contains binary data inside them, you can store numbers or even a binary string (common strings are represented as lists internally)
Bin1 = <[1, 2, 3, 4]>
you can have a string represented as a binary
Bin2 = <["mariano"]>
an atom is a named constant. It does not have an explicit value.
A = foo
T = (foo, bar, baz)
they may seem useless at first, but believe me, they will be useful.
functions are declared with the following format:
<name> = fn ([<arguments>]) {
<body>
}
<name> = fn ([<arguments>])
<body>
where arguments are optional
an actual example
divide = fn (A, B) {
A / B
}
divide = fn (A, B)
A / B
see that we don’t declare types, and the returned value is the last expression evaluated.
now we can call the function
divide(10, 5)
we know that we can’t divide by 0, so we can use pattern matching for that
divide = fn (A, 0) {
error
}
fn (A, B) {
A / B
}
divide = fn (A, 0)
error
fn (A, B)
A / B
here we give two definitions for the function, the first “matches” when the second argument is zero and returns the error atom.
the second definition is more generic and will match everything that didn’t matched the previous definitions.
we can declare functions and assign them to variables and pass them around like any common variable, the syntax is the same and you can do the same things.
SayHi = fn (Name) {
io.format("hi ~s!~n", [Name])
}
SayHi("efene")
SayHi = fn (Name)
io.format("hi ~s!~n", [Name])
SayHi("efene")
we can restrict our functions adding guards, that check conditions on the arguments before calling them, we could restrict our previous function to allow only numbers
divide = fn (A, 0) when is_number(A) {
error
}
fn (A, B) when is_number(A) and is_number(B){
A / B
}
divide = fn (A, 0) when is_number(A)
error
fn (A, B) when is_number(A) and is_number(B)
A / B
here we say that the function will be called only when A and B are numbers.
if GuardSeq1 {
Body1
}
else if GuardSeqN {
BodyN
}
else {
ElseBody
}
if GuardSeq1
Body1
else if GuardSeqN
BodyN
else
ElseBody
The branches of an if-expression are scanned sequentially until a guard sequence GuardSeq which evaluates to true is found. Then the corresponding Body is evaluated.
The return value of Body is the return value of the if expression.
If no guard sequence is true, an if_clause run-time error will occur. If necessary, else can be used in the last branch, as that guard sequence is always true.
parenthesis around GuardSeq are optional
an example
Num = random.uniform(2)
# without parenthesis
if Num == 1 {
io.format("is one~n")
}
else {
io.format("not one~n")
}
# with parenthesis
if (Num == 1) {
io.format("is one~n")
}
else if (Num == 2) {
io.format("is two~n")
}
else {
io.format("not one or two~n")
}
Num = random.uniform(2)
# without parenthesis
if Num == 1
io.format("is one~n")
else
io.format("not one~n")
# with parenthesis
if (Num == 1)
io.format("is one~n")
else if (Num == 2)
io.format("is two~n")
else
io.format("not one or two~n")
switch Expr {
case Pattern1 [when GuardSeq1] {
Body1
}
case PatternN [when GuardSeqN] {
BodyN
}
else {
BodyElse
}
}
switch Expr
case Pattern1 [when GuardSeq1]
Body1
case PatternN [when GuardSeqN]
BodyN
else
BodyElse
The expression Expr is evaluated and the patterns Pattern are sequentially matched against the result. If a match succeeds and the optional guard sequence GuardSeq is true, the corresponding Body is evaluated.
The return value of Body is the return value of the case expression.
If there is no matching pattern with a true guard sequence, a case_clause run-time error will occur.
parenthesis around the Expr, Patterns and GuardSeq is optional
an example
switch random.uniform(4) {
case 1 {
# without parens
io.format("one!~n")
}
case (2) {
# with parens
io.format("two!~n")
}
case (3) {
io.format("three!~n")
}
else {
io.format("else~n")
}
}
switch random.uniform(4)
case 1
# without parens
io.format("one!~n")
case (2)
# with parens
io.format("two!~n")
case (3)
io.format("three!~n")
else
io.format("else~n")
this statement allows to handle errors that can appear while running an expression.
try {
Exprs
}
catch [Class1:]ExceptionPattern1 [when ExceptionGuardSeq1] {
ExceptionBody1
}
catch [ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] {
ExceptionBodyN
}
after {
AfterBody
}
try
Exprs
catch [Class1:]ExceptionPattern1 [when ExceptionGuardSeq1]
ExceptionBody1
catch [ClassN:]ExceptionPatternN [when ExceptionGuardSeqN]
ExceptionBodyN
after
AfterBody
Returns the value of Exprs (a sequence of expressions Expr1, …, ExprN) unless an exception occurs during the evaluation. In that case the exception is caught and the patterns ExceptionPattern with the right exception class Class are sequentially matched against the caught exception. An omitted Class is shorthand for throw. If a match succeeds and the optional guard sequence ExceptionGuardSeq is true, the corresponding ExceptionBody is evaluated to become the return value.
If an exception occurs during evaluation of Exprs but there is no matching ExceptionPattern of the right Class with a true guard sequence, the exception is passed on as if Exprs had not been enclosed in a try expression.
If an exception occurs during evaluation of ExceptionBody it is not caught.
The after block will be executed no matter an exception was thrown or not
parenthesis around Exprs and catch patterns are optional
an example
try {
make_error(Arg)
}
catch (throw 1) {
# catch with parenthesis
io.format("throw 1~n")
}
catch exit 2 {
# catch without parenthesis
io.format("exit 2~n")
}
catch error (some fancy tuple 3) {
io.format("error~n")
}
after {
io.format("else~n")
}
try
make_error(Arg)
catch (throw 1)
# catch with parenthesis
io.format("throw 1~n")
catch exit 2
# catch without parenthesis
io.format("exit 2~n")
catch error (some fancy tuple 3)
io.format("error~n")
after
io.format("after~n")
receive Pattern1 [when GuardSeq1] {
Body1;
}
else receive PatternN [when GuardSeqN] {
BodyN
}
after Milliseconds {
AfterBody
}
receive Pattern1 [when GuardSeq1]
Body1
else receive PatternN [when GuardSeqN]
BodyN
after Milliseconds
AfterBody
Receives messages sent to the process using the send operator (!). The patterns Pattern are sequentially matched against the first message in time order in the mailbox, then the second, and so on. If a match succeeds and the optional guard sequence GuardSeq is true, the corresponding Body is evaluated. The matching message is consumed, that is removed from the mailbox, while any other messages in the mailbox remain unchanged.
The return value of Body is the return value of the receive expression.
receive never fails. Execution is suspended, possibly indefinitely, until a message arrives that does match one of the patterns and with a true guard sequence.
the after block is optional and can be used to specify a timeout after which AfterBody will be executed.
parenthesis around patterns and guards are optional
an example
receive "some string" {
ok
}
else receive (5) {
five
}
else receive true {
true
}
after 100 {
io.format(".")
}
receive "some string"
ok
else receive (5)
five
else receive true
true
after 100
io.format(".")
records are the same as in erlang.
http://www.erlang.org/doc/reference_manual/records.html
the syntax to declare a new record type
@rec(recordname) -> (attr1[=defaultvalue1], attr2[=defaultvalue2])
the syntax to instantiate a record is
Var = recordname[attr1=value1, attr2=value2, ...]
for example a person record
@rec(person) -> (firstname, lastname, mail)
R = person[firstname="Bob", lastname="Esponja", mail="bob
esponja.com"]@
to modify an existing record and assign it to a new variable
R1 = person.R[firstname="Mariano", lastname="Guerra"]
to access a record attribute
person.R[firstname]
this items are not advanced because of their complexity, only they are advanced because you may need them when you master the items explained above.
the char operator ‘$’ allows to obtain the ascii value of a character as integer
$a # evaluates to 97
arrow expressions allow to pass the result of an expression to the next one as first argument of the function call, if multiple arrow expressions are chained then the result of the previous expression is passed.
this expression is useful to do multiple modifications to some value without the need of temporary variables or nested expressions that are hard to read in comparison to chained expressions.
the example shows the usage of the l.fn module to manipulate a list multiple times (the escape sequence ‘\’ is used to break the long expression into multiple lines)
# create a list with the numbers from 1 to 10
l.range(from, 2, to, 10)\
# increment each item by 1
->l.map(fn (X) { X + 1 })\
# keep the even numbers on the list
->l.keep(fn (X) { X % 2 == 0 })\
# print the result
->l.print()\
# reverse the list
->l.reverse()\
# print the result again
->l.print()\
# call some metht]) })\
# append some items
->l.append([30, 31, 32])\
# do something with each item
->l.each(fn (Item) { io.format("double: ~p~n", [Item * 2]) })\
# remove the values above 20
->l.remove(fn (Item) { Item > 20 })\
# print it
->l.print()
A list comprehension is a syntactic construct for creating a list based on existing lists.
A = [X for X in lists.seq(1, 10)]
B = [X for X in lists.seq(1, 10) if X % 2 == 0]
C = [X for X in lists.seq(1, 10) if X % 2 == 0 and X != 4]
io.format("~p~n~p~n~p~n", [A, B, C])
the result is
[1,2,3,4,5,6,7,8,9,10] [2,4,6,8,10] [2,6,8,10]
a more complex example
[(A, B, C) for A in lists.seq(1, N) \
for B in lists.seq(A, N) \
for C in lists.seq(B, N) \
if A + B + C <= N and A * A + B * B == C * C]
like list comprehensions but for binaries
B = <["mariano"]>
B1 = <[Char - 32 for <[Char:8]> in B]>
if you need to pass a reference to a function as parameter to another function you can refer to the function by giving its name (and module if necessary) and the arity of the function (that means the number of arguments it receives).
fn lists.append:2
refers to this function
pattern matching is available everywhere in efene (and erlang) and can be really useful, for example we can pattern match a value against each other to get some values.
fun = fn ((_, T, (N, _))=A) {
io.format("~p ~p ~p~n", [T, N, A])
}
run = fn () {
A = (complex, tuple, (1, true))
(_, T, (N, _)) = A
io.format("~p ~p~n", [T, N])
fun(A)
}
fun = fn ((_, T, (N, _))=A)
io.format("~p ~p ~p~n", [T, N, A])
run = fn ()
A = (complex, tuple, (1, true))
(_, T, (N, _)) = A
io.format("~p ~p~n", [T, N])
fun(A)
see in run how we extract some values of A into T and N.
in fun you can see how the same values are extracted but also the whole tuple is assigned to A.
the result of running this is
tuple 1 tuple 1 {complex,tuple,{1,true}}
In computer science, a closure is a first-class function with free variables that are bound in the lexical environment.
in simple words it’s a function that has reference to variables declared outside of it, but its values are bound inside the function.
let’s see an example
runIt = fn (Fun) {
Fun()
}
main = fn () {
Name = "mariano"
SayHi = { io.format("hi ~s!~n", [Name]) }
runIt(SayHi)
}
runIt = fn (Fun)
Fun()
main = fn ()
Name = "mariano"
SayHi = { io.format("hi ~s!~n", [Name]) }
runIt(SayHi)
the function runIt receives a function and runs it, in main we define a Name variable and we use it inside the SayHi function, see that the Name variable is bound inside the function even when the variable is not declared inside it. That’s a closure.
you can also see that since SayHi doesn’t receive arguments we don’t need to write
SayHi = fn () { io.format("hi ~s!~n", [Name]) }
we can if we want but it’s clearer in the first way.