layout | title | category | tags | order | ||
---|---|---|---|---|---|---|
developer-doc |
Defining Functions |
syntax |
|
10 |
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.
- Lambdas
- Defining Functions
- Methods
- Calling Functions and Methods
- Code Blocks
- Operators
- Mixfix Functions
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.
- Lambdas can close over variables in their surrounding scope.
- If you want to define a multi-argument lambda, you can do it by having a
lambda return another lambda (e.g.
a -> b -> a + b
).
Additionally, lambdas in Enso have the following properties:
- The lambda introduces a new scope shared by the left and right operands.
- The left operand introduces a pattern context.
- If a lambda occurs in a pattern context, its left-hand-side identifiers are
introduced into the scope targeted by the outer pattern context. For example,
the following is valid
(a -> b) -> a.default + b
. - Lambdas cannot currently occur in a matching context.
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 this
argument) is associated
with a given atom. Methods are dispatched dynamically based on the type of the
this
argument, while functions are not.
Methods can be defined in Enso in two ways:
- In the Body of a Type: A function defined in the body of a
type
definition is automatically converted to a method on all the atoms defined in the body of that type definition.
type Maybe a
Nothing
type Just (value : a)
isJust = case this of
Nothing -> False
Just _ -> True
- As an Extension Method: A function defined explicitly on an atom counts as an extension method on that atom. It can be defined on a typeset to apply to all the atoms within that typeset.
Number.floor = case this of
Integer -> ...
...
- As a Function with an Explicit
this
Argument: A function defined with the type of thethis
argument specified to be a type.
floor (this : Number) = case this of
Integer -> ...
If the user does not explicitly specify the this
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.
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.
- To call a function
f
on argumentsa
andb
, we writef a b
. - To call a method
f
defined on a typeA
(valuea
, here) on argumentb
, we writea.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
andb
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
wherea
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:
- Code blocks are desugared into in-order applications of monadic bind (as in keeping with the fact that all blocks are monadic contexts).
- If an expression that returns a value is not assigned to an identifier, this will issue a warning.
- To suppress this warning you can assign it to a blank (
_
).
test =
_ = expr1
expr2
# Becomes
test =
expr1 >>= (_ -> expr2)
# Equivalent to
test =
expr1 >> expr2
- If the trailing line of the block (the return value) is an assignment, it will
return
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:
- Definition: This defines a function that is called on the arguments provided to the operator.
- Precedence: This is an optional block that defines the precedence relation for the operator. Precedence in Enso is specified in relation to existing operators. If you do not provide this information, no precedence relations will be defined.
- Associativity: This is an optional block that defines the
operator associativity
to be either
left
,right
, ornone
. If you do not provide this, the operator's associativity will default toleft
.
@prec [> *, < $]
@assoc left
^ a n = a * a ^ (n-1)
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.
- Precedence is not set at fixed levels, but is instead defined in relation to the precedence of other operators.
- Precedence of an operator in Enso depends on whether a particular operator is
surrounded by spaces or not. This means that the precedence of any operator
not surrounded by spaces is always higher than the precedence of any operator
surrounded by spaces. The only exception to this rule is the
,
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.
- Where an argument is not applied to an operator, the missing argument is
replaced by an implicit
_
. - The application is then translated based upon the rules for underscore arguments described later.
- The whitespace-based precedence rules discussed above also apply to operator sections.
A mixfix function is a function that is made up of multiple sections. They are defined using a special syntax, and operate as follows:
- They are defined using a 'split snake case'. The first section is written as
normal, but subsequent sections are prefixed by an underscore (
if c _then a
, for example). - The layout rules applied to mixfix functions operate as if each section was a separate operator, allowing you to write an indented block of code after each section.
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