-
Notifications
You must be signed in to change notification settings - Fork 32
LanguageReference
Variables start with uppercase and can’t be set twice (single assignment)
A = 2
to change it’s value assign to a new variable
B = A + 1
each expressions ends with a new line, you can make multiline extensions escaping the new line with the ‘\’ character
E = 12 + \
5 * 2
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]
commas are not required
List1 = [2 3 4 5]
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)
tuples can also be specified without commas
Tuple1 = (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>
}
where arguments are optional
an actual example
divide = fn (A B) {
A / B
}
see that we don’t declare types, and the returned value is the last expression evaluated.
commas on the arguments are optional
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
} (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")
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
} (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
} GuardSeqN {
BodyN
}
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")
} (Num == 2) {
io.format("is two~n")
} else {
io.format("not one or two~n")
}
case Expr {
Pattern1 [when GuardSeq1] {
Body1
} PatternN [when GuardSeqN] {
BodyN
}
}
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
case random.uniform(4) {
1 {
# without parens
io.format("one!~n")
} (2) {
# with parens
io.format("two!~n")
} (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
} [ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] {
ExceptionBodyN
}
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.
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")
} exit 2 {
# catch without parenthesis
io.format("exit 2~n")
} error (some fancy tuple 3) {
io.format("error~n")
} else {
io.format("else~n")
}
receive Pattern1 [when GuardSeq1] {
Body1;
} PatternN [when GuardSeqN] {
BodyN
}
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.
parenthesis around patterns and guards are optional
an example
receive "some string" {
ok
} (5) {
five
} true {
true
} after 100 {
io.format(".")
}
objects are a way to define entities that contain some information with some basic operations on them. They are meant to replace records in erlang with a clear syntax and some dynamic features that records lack.
an object is declared as a toplevel expression on a .fn file as follows:
<name> = object(field1 [field2 [field3 ...]])
an actual example
person = object(firstname lastname mail)
then we can create and manipulate this objects
# helper function
Print = fn (X) { io.format("~p~n" [X]) }
# create an "object"
P = person("mariano" "guerra" "mail")
# get firstname
Print(P(get firstname))
# get lastname
Print(P(get lastname))
# get mail
Print(P(get mail))
# get the "object" as an erlang record
Print(P(to record))
# get the fields of the "object"
Print(P(to fields))
# get the name of the "object"
Print(P(to name))
# check if the "object" has an attr called firstname
Print(P(has firstname))
# check if the "object" has an attr called address
Print(P(has address))
# create a new "object" changing the firstname attribute
P1 = P(setfirstname "Mariano")
# print the new "object"
Print(P1(to record))
# build a new person from the record of another one
P2 = person(P1(to record))
Print(P2(to record))
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 method with the list as parameter
# (tap doesn't modify the list)
->l.tap(fn (List) { io.format("the list is: ~p~n" [List]) })\
# 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).
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)
}
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)
}
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.