docs/syntax/functions.md
Enso is a purely-functional programming language. As a result it has support for first-class and higher-order functions, meaning that you can pass functions as arguments to other functions, return functions from functions, assign them to variables, store them in data structures and so on.
Functions in Enso are curried by default, meaning that all functions are actually functions in one argument, but may return functions accepting further arguments.
<!-- MarkdownTOC levels="2,3" autolink="true" -->A lambda is an anonymous function introduced by the \ operator, followed by
any arguments definitions, followed by the -> operator and a body expression,
e.g. \a b -> a + b.
Argument definitions: Each argument may have any combination of:
~: Type= value (when used in conjunction with a type declaration,
the type declaration must come first)The full syntax is: \((~a:Type) = value) -> a. The parentheses can be omitted
when they contain no spaces.
Lambdas can close over variables in their surrounding scope.
(The original lambda syntax, described below, will eventually be removed.)
The most primitive non-atom construct in Enso is the lambda. This is an
anonymous function in one argument. A lambda is defined using the -> operator,
where the left hand side is an argument, and the right hand side is the body of
the function (containing arbitrary code).
Some functional languages such as Haskell allow for the definition of a lambda
with multiple arguments, but in Enso the type signature use of ->and the
lambda use of -> are one and the same. We do not want to have to put the
components of a type signature in parentheses, so we only allow one argument
before each arrow.
a -> b -> a + b).Additionally, lambdas in Enso have the following properties:
(a -> b) -> a.default + b.Please note that if a later lambda in a chain shadows an earlier lambda (e.g.
a -> a -> a), the shadowed arguments by that name are inaccessible. If you
want to unify later arguments with previous ones, you must employ the scope
reference rule and write (in this case) a -> A -> a.
The actionables for this section are:
- In the future we want to be able to match on function types, so this restriction should be relaxed.
- Do we want any automated unification to take place in the shadowing case?
A function definition is just syntactic sugar for the definition of a lambda, and hence has all the properties that a lambda does. Syntactically, functions are defined in a similar way to variables. The only difference is that the function name is followed by one or more parameters.
sum x y = x + y
Under the hood, functions are desugared to a lambda assigned to a variable that binds the function name. This means that:
Like any variable, you can use the : type ascription operator to provide a
user-defined type for the function.
sum : (a:Monoid) -> a -> a
sum : x -> y -> x + y
sum x y = x + y
Functions have an arity. Unlike a single lambda which always has an arity of one, function arity refers to the number of arguments in the function definition, which may not always be deduced from the type signature, but may still be inferred.
Enso makes a distinction between functions and methods. In Enso, a method is a
function where the first argument (known as the self argument) is associated
with a given atom. Methods are dispatched dynamically based on the type of the
self argument, while functions are not.
Methods can be defined in Enso in two ways:
type
definition is automatically converted to a method on all the atoms defined in
the body of that type definition. If the function has self parameter, it is
called an instance method. If the function does not have self parameter,
it is called a static method.type Maybe a
Nothing
Just (value : a)
is_just self = case self of
Maybe.Nothing -> False
Maybe.Just _ -> True
self argument is present or not.Number.floor self = case self of
Integer -> ...
...
self argument, is considered a module method.module_method x y = x + y
If the user does not explicitly specify the self argument by name when
defining a method (e.g. they use the Type.name syntax), it is implicitly added
to the start of the argument list.
Note that the following methods defined in the body of type and as an extension method are equivalent:
type My_Type
method self = 42
My_Type.method self = 42
Enso makes the distinction between functions and methods. Methods are entities that are dispatched dynamically and looked up at runtime, while functions are defined locally and are looked up at compile time. In order to provide good diagnostics, we distinguish between how functions and methods are called.
f on arguments a and b, we write f a b.f defined on a type A (value a, here) on argument b,
we write a.f b. This instance method can also be called via a static
method with A.f a b, which is equivalent to a.f b.Top-level blocks in the language are evaluated immediately. This means that the layout of the code has no impact on semantics of the code:
This means that the following a and b are equivalent.
a = foo x y
b =
foo x y
To suspend blocks, we provide a suspend function in the standard library.
This function takes any expression as an argument (including a block), and suspends the execution of that expression such that it is not evaluated until forced later.
susp = suspend
x = foo x y z
x.do_thing
Alternatively, it is sufficient to type the binding for the block as
Suspended a where a is the type of the block.
susp : Suspended a =
x = foo x y z
x.do_thing
It should be noted that this does not yet work.
The following rules apply to code blocks:
_).test =
_ = expr1
expr2
# Becomes
test =
expr1 >>= (_ -> expr2)
# Equivalent to
test =
expr1 >> expr2
Nothing as all assignments do.foo =
pat1 = expr1
# Becomes
foo =
expr1 >>= (pat1 -> Nothing)
In Enso, an operator is a function with a non-alphanumeric name (e.g. +). We
only support binary operators, with left and right arguments.
Enso provides a significant amount of flexibility for developers who want to
define custom operators. Formally, any sequence of the following characters
forms an operators .!$%&*+-/<>?^~\. Operator definitions have three main
parts:
left, right, or none. If you do not provide this, the
operator's associativity will default to left.@prec [> *, < $]
@assoc left
^ a n = a * a ^ (n-1)
Just like with any other function definition in Enso operator arguments may be
associated with type ascriptions. Having a type Num we can add + operator
to it as:
Num.+ self that:Num -> Num = # add somehow
This inlined ascribed arguments increase type safety of the program by checking
the types of values passed as arguments at runtime and yielding a Type_Error
automatically when the types don't match. Moreover they support flexibility by
automatically using conversions when the actual argument values don't match,
but there is a way to make them match. Should there be a conversion from an
Integer:
Num.from (that:Integer) = # convert somehow
then one can invoke the + on Num with Integer argument:
add_five n:Num = n+5
the above statement first converts the Integer literal 5 used as second
argument of the operator to Num using the above defined Num.from conversion
method and then it invokes the Num.+ operator with n and Num value
representing 5 obtained from the conversion method. This is a regular behavior
of every Enso function.
However, in order to support extensibility of types, the operators also offer
additional resolution based on that argument. Because (while it is possible
to write add_five method as shown above) the following wouldn't be properly
typed according to standard Enso function dispatch rules:
five_add n:Num = 5+n
this would fail as the Integer.+ expects both arguments to be Integer and
here we are trying to pass Num as the second argument. Should + be a regular
function (and not an operator) we would get Type_Error. However the special
binary operator resolution based on that argument kicks in and finds out
the that argument type is Num and the type Num also defines + operator.
Moreover there is a conversion from Integer (type of the self argument) to
Num. Hence the Enso runtime system decides to convert 5 to Num and perform
the addition by invoking Num.+. This behavior allows one to write libraries
that extend existing number types with Complex_Number, Rational_Number and
make them behave as first class citizen numbers.
The == operator is special. A consistency with hash code is necessary to make
any Enso object behave correctly and work effectively in Set and Map
implementations. To guarantee such level of consistency there is a Any.==
definition providing universal equality that shall not be overriden.
The == behavior is predefined for builtin types, atoms and other Enso objects.
In addition to that it remains possible to define own comparators, including a
comparator capable to work with already existing types. To create such
comparator define:
Ordering type)To extend the previous definition of Num also for equality one might do for
example:
type Num_Comparator
compare a:Num b:Num = # compare somehow
hash a:Num = # hash somehow
Num.from (that:Integer) = # convert somehow
Comparable.from (_:Num) = Num_Comparator
Comparable.from (_:Integer) = Num_Comparator
with such a structure the internal implementation of Any.== performs necessary
conversions of Integer argument in case the other argument is Num and
invokes the Num_Comparator.compare to handle the comparision.
A care must be taken to keep consistency between hash values of original and
converted types - e.g. hash of n:Integer and hash of Num.from n must be the
same (otherwise consistency required for Set and Map would be compromised).
Operator precedence in Enso is a collection of rules that reflect conventions about which operations to perform first in order to evaluate a given expression that contains operators. However, operator precedence in Enso differs from many other programming languages.
, operator,
which retains the same precedence level regardless of whether it is surrounded
by spaces or not.This space-based precedence may seem strange coming from other languages, but it allows for writing far cleaner code than other functional languages. This is best demonstrated by example. Consider the following code:
list = 1 .. 100
randomList = list . each random
headOfList = randomList . take 10
result = headOfList . sort
This could easily be refactored to the following one-liner:
result = (((1 .. 100).each random).take 10).sort
This is still quite noisy, however, so using the whitespace-sensitive operator
precedence rules, combined with the fact that the operator . is a regular
operator, we get the following.
result = 1..100 . each random . take 10 . sort
An operator section is a nice shorthand for partially applying an operator. It works as follows:
[1, 2, 3].reduce (+) . should_equal 6
[1, 2, 3].reduce function=+ . should_equal 6
[1, 2, 3].map (1 /) . should_equal [1.0, 0.5, 0.3333333333333333]
[1, 2, 3].map (/ 1) . should_equal [1.0, 2.0, 3.0]
[1, 2, 3].map function=1/ . should_equal [1.0, 0.5, 0.3333333333333333]
[1, 2, 3].map function=/1 . should_equal [1.0, 2.0, 3.0]
[1, 2, 3].map 1/ . should_equal [1.0, 0.5, 0.3333333333333333]
[1, 2, 3].map /1 . should_equal [1.0, 2.0, 3.0]
A mixfix function is a function that is made up of multiple sections. They are defined using a special syntax, and operate as follows:
if c _then a,
for example).Probably the best-known example of a mixfix function is if-then-else, which is
indeed defined in the Enso standard library.
if foo == bar then frob else
thing1
thing2