Skip to content
marianoguerra edited this page Sep 13, 2010 · 12 revisions

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.

basics

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

expressions

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)

datatypes

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

functions are declared with the following format:

efene


<name> = fn ([<arguments>]) { 
    <body>
}

ifene


<name> = fn ([<arguments>])
    <body>

where arguments are optional

an actual example

efene


divide = fn (A, B) { 
    A / B
}

ifene


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

efene


divide = fn (A, 0) {
   error
} 
fn (A, B) { 
    A / B
}

ifene


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.

efene


SayHi = fn (Name) {
        io.format("hi ~s!~n", [Name])
}

SayHi("efene")

ifene


SayHi = fn (Name)
        io.format("hi ~s!~n", [Name])

SayHi("efene")

guards

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

efene


divide = fn (A, 0) when is_number(A) {
   error
} 
fn (A, B) when is_number(A) and is_number(B){
    A / B
}

ifene


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.

blocks

if

efene


if GuardSeq1 {
        Body1
} 
else if GuardSeqN {
        BodyN
}
else {
        ElseBody
}

ifene


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

efene


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")
}

ifene


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")

case

efene


switch Expr {
    case Pattern1 [when GuardSeq1] {
        Body1
     }
    case PatternN [when GuardSeqN] {
        BodyN
    }
    else {
        BodyElse
     }
}

ifene


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

efene


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")
    }
}

ifene


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")

try catch

this statement allows to handle errors that can appear while running an expression.

efene


try {
        Exprs
} 
catch [Class1:]ExceptionPattern1 [when ExceptionGuardSeq1] {
        ExceptionBody1
} 
catch [ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] {
        ExceptionBodyN
}
after {
        AfterBody
}

ifene


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

efene


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")
}

ifene


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

efene


receive Pattern1 [when GuardSeq1] {
        Body1;
} 
else receive PatternN [when GuardSeqN] {
        BodyN
}
after Milliseconds {
        AfterBody
}

ifene


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

efene


receive "some string" {
    ok
} 
else receive (5) {
    five
} 
else receive true {
    true
} 
after 100 {
    io.format(".")
}

ifene


receive "some string"
    ok
else receive (5)
    five
else receive true
    true
after 100
    io.format(".")

records

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="bobesponja.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]

advanced

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.

char operator

the char operator ‘$’ allows to obtain the ascii value of a character as integer

$a # evaluates to 97

arrow expressions

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()

list comprehensions

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]

binary comprehensions

like list comprehensions but for binaries


B = <["mariano"]>
B1 = <[Char - 32 for <[Char:8]> in B]>

referring to functions

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

advanced pattern matching

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.

efene


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)
}

ifene


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}}

closures and functions without parameters

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

efene


runIt = fn (Fun) {
    Fun()
}

main = fn () {
    Name = "mariano"

    SayHi = { io.format("hi ~s!~n", [Name]) }

    runIt(SayHi)
}

ifene


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.