Expressions
Expressions in Catala represent the meat of computation rules, that appear in scope variable definitions or in global constant definitions.
Quick reference of the expressions' BNF grammar
Quick reference of the expressions' BNF grammar
The Backus-Naur Form (BNF) grammar is a convenient format for summarizing what counts as an expression in Catala. If you're familiar with this format, you can read it below:
<expr> ::=
| (<expr 1>, <expr 2>, ...) # Tuple
| <expr>.<field name> # Structure field access
| <expr>.<integer> # Tuple component access
| [<expr 1>; <expr 2>; ...] # List
| <structure> { -- <field name 1>: <expr 1> -- ...} # Structure value
| <enum variant> content <expr> # Enum value
| <expr 1> of <expr 2>, <expr 3>, ... # Function call
| output of <scope name> with # Direct scope call
{ -- <variable 1>: <expr 1> -- ... }
| match <expr> with pattern # Pattern matching
-- <enum variant 1>: <expr 1>
-- <enum variant 2> of <variable>: <expr 2>
| <expr> with pattern <enum variant> # Pattern variant test
| <expr> with pattern <enum variant> of <variable> # Ditto with content binding
| <expr> but replace # Structure partial updates
{ -- <variable 1>: <expr 1> -- ... }
| - <expr> # Negation
| <expr>
< + - * / and or not xor > < >= <= == != > <expr> # Binary operations
| if <expr 1> then <expr 2> else <expr 3> # Conditionals
| let <variable> equals <expr 1> in <expr 2> # Local let-binding
| ... # Variable, literals,
# list operators, ...
References to other variables
Inside an expression, you can refer to the name of other variables of the same scope, or to toplevel constants and functions.
Some scope variables can have multiple states.
Suppose you have scope variable foo
has states bar
and baz
in this order.
You can either refer to foo
, foo state bar
or foo state baz
, but the ability
or meaning of these reference depend on the context according to the following
rules.
- Inside the expression of
definition foo state bar
, you cannot refer tofoo
, norfoo state bar
neitherfoo state baz
, sincebar
is the first state andfoo
is being defined for the first state. - Inside the expression of
definition foo state baz
, you can refer tofoo
and it will actually refer to the previously defined state forfoo
, herebar
. Sofoo
andfoo state bar
are equivalent in this context, and you cannot refer tofoo state baz
since it is being defined. - Outside the definitions of
foo
, you can refer tofoo state bar
andfoo state baz
. It you refer simply tofoo
, it will default to the last state, herebaz
. - If
foo
in aninput
variable of the scope, then its first state cannot be defined and will be valued by the argument of the scope when it is being called.
To reference a variable from another module, use the syntax
<name of module>.<name of variable>
.
Values and operations
All the values and operations previously presented are fully-fledged expressions.
Parenthesis
You can use parenthesis (...)
around any part or sub-part of an expression
to make sure that the compiler will understand correctly what you are typing.
Local variables and let-bindings
Inside a complex definition
of a scope variable, it is often useful to give a name to an
intermediate quantity to promote its reuse, or simply to make the code more
readable. While it
is always possible to introduce a new scope
variable to that effect, you can also use a
lighter local variable that only affects the current expression. The syntax for these is let foo equals ... in ...
. For instance :
scope Bar:
definition baz equals
let foo equals [4; 6; 5; 1] in
sum integer of foo - maximum of foo
If you have a value x
of type (integer, boolean)
, you can use
x.0
and x.1
to access the two components of the tuple. But you can also
bind the two components to two new variables y
and z
with:
let (y, z) = x in
if z then y else 0
This syntax mirrors the more general use of patterns in let-bindings in functional programming languages like OCaml and Haskell. However, for the moment, only tuples can be destructured like that.
Conditionals
You are encouraged to use exceptions to scope variable definitions to encode the base case/exception logic of legal texts. Only exceptions and conditional definitions of scope variables allow you to split your Catala code into small chunks, each attached to the piece of legal text it encodes.
However, sometimes, it makes just sense to use a regular old conditional inside an expression to
distinguish between two cases. In that case, use the traditional if ... then ... else ...
. Note
that you have to include the else
everytime since this conditional is an expression always
yielding a value and not a statement that conditionally updates some memory cell.
Structures
As explained previously, structure values are built with the following syntax:
Individual {
-- birth_date: |1930-09-11|
-- income: $100,000
-- number_of_children: 2
}
To access the field of a structure, simply use the syntax individual.income
.
Suppose you have a value foo
containing a big structure Bar
with a dozen fields,
including baz
. You want to obtain a new structure value similar to foo
but with a different value for bar. You could write:
Bar {
-- baz: 42
-- fizz: foo.fizz
-- ...
}
But this is very tedious as you have to copy over all the fields. Instead, you can write:
foo but replace { -- baz: 42 }
Enumerations
As explained previously, the type of each case of the enumeration is
mandatory and introduced by content
. It is possible to nest enumerations (declaring the type of a
field of an enumeration as another enumeration or structure), but not recursively.
Enumeration values are built with the following syntax:
# First case
NoTaxCredit
# Second case
TaxCreditForIndividual content (Individual {
-- birth_date: |1930-09-11|
-- income: $100,000
-- number_of_children: 2
})
# Third case
TaxCreditAfterDate content |2000-01-01|
Pattern matching
Pattern matching is a popular programming language feature that comes from functional programming, introduced in the mainstream by Rust, but followed by other languages like Java or Python. In Catala, pattern matching works on enumeration values whose type has been declared by the user. Suppose you have declared the type
declaration enumeration NoTaxCredit:
-- NoTaxCredit
-- TaxCreditForIndividual content Individual
-- TaxCreditAfterDate content date
and you have a value foo
of type NoTaxCredit
. foo
is either an instance
of NoTaxCredit
, or TaxCreditForIndividual
, or TaxCreditAfterDate
. If
you want to use foo
, you have to provide instructions for what to do in each of
the three cases, since you don't know in advance which one it will be. This
is the purpose of pattern matching; in each of the case, provide an
expression yielding what should be the result in this case. These case-expressions
can also use the contents stored inside the case of the enumerations, making
pattern matching a powerful an intuitive way to "inspect" nested content.
For instance, here is the pattern matching syntax to compute the tax credit
in our example:
match foo with pattern
-- NoTaxCredit: $0
-- TaxCreditForIndividual of individual: individual.income * 10%
-- TaxCreditAfterDate of date: if today >= date then $1000 else $0
In TaxCreditForIndividual of individual
, while TaxCreditForIndividual
is
the name of the enumeration case being inspected, individual
is a user-chosen
variable name standing for the content of this enumeration case. In other words:
you can choose your own name for the variable in the syntax at this location!
Importantly, pattern matching also helps you avoid forgetting cases to handle. Indeed, if you declare a case in the type but forget it in the pattern matching, you will get a compiler error.
Often, the result of the pattern matching should be the same in a lot of cases,
leading you to repeat the same result expression for each enumeration case.
For conciseness and precision, you can use the anything
catch-all case as
the last case of your pattern matching. For instance, here this computes whether
you should apply a tax credit or not:
match foo with pattern
-- NoTaxCredit: true
-- anything: false
You can create a boolean test for a specific case of an enum value with pattern matching:
match foo with pattern
-- TaxCreditForIndividual of individual: true
-- anything: false
However, writing this full pattern matching for a simple boolean test of a specific case is cumbersome. Catala offers a sugar to make things more concise; the code below is exactly equivalent to the code above.
foo with pattern TaxCreditForIndividual
Now suppose you want to test whether foo
is TaxCreditForIndividual
and that the individual
's income is greater than $10,000. You could write:
match foo with pattern
-- TaxCreditForIndividual of individual: individual.income >= $10,000
-- anything: false
But instead you can also write the more concise:
foo with pattern TaxCreditForIndividual of individual and individual.income >= $10,000
Is Catala's pattern matching as powerful as OCaml or Haskell's?
Is Catala's pattern matching as powerful as OCaml or Haskell's?
No, currently Catala's pattern matching is bare-bones and allows only to match the outer-most enumeration type of the value. The Catala team has plans to gradually implement more advanced pattern matching features, but they have not yet been implemented.
Tuples
As explained previously, you can build tuple values with the following syntax:
(|2024-04-01|, $30, 1%) # This values has type (date, money, decimal)
You can also access the n
-th element of a tuple, starting at 1
, with the syntax <tuple>.n
.
Lists
You can build list values using the following syntax:
[1; 6; -4; 846645; 0]
All the operations available for lists are available on the relevant reference page.
Function calls
To call function foo
with arguments 1
, baz
and true
, the syntax is:
foo of 1, baz, true
The functions that you can call are either user-defined toplevel functions,
or builtin operators like get_day
. To call
a scope like a function, see just below.
Direct scope calls
The Catala team advocates using sub-scope
declarations and sub-scope
calling when possible (with
a single, static sub-scope call), because it enables using conditional
definitions and exceptions on the arguments of the sub-scope. However, sometimes
a scope has to be called dynamically under certain conditions or inside a loop,
which makes impossible to use the former mechanism. In these situations, you can
use direct scope calls which are the equivalent of direct function calls, but
for scopes, as an expression. For instance, suppose you are inside an expression
and want to call scope Foo
with arguments bar
and baz
; the syntax is:
result of Foo with {
-- bar: 0
-- baz: true
}
Note that the value returned by the above is the scope output
structure of Foo
, containing
one field per output variable. You can store this output value in a local
variable and then
access its fields to retrieve the values
for each output variable.
"Impossible" cases
When some cases are not expected to happen in the normal execution flow of a
program, they can be marked as impossible
. This makes the intent of the
programmer clear, and removes the need to write a place-holder value. If, during
execution, impossible
is reached, the program will abort with a fatal error.
It is advised to always accompany impossible
with a comment justifying why the
case is deemed impossible.
impossible
has type anything
, so that it can be used in place of any value.
For example:
match foo with pattern
-- TaxCreditForIndividual of individual : individual.birth_date
-- anything :
impossible # We know that foo is not in any other form at this point because...
Be careful that any value that is not guarded by conditions may be computed,
even if not directly needed to compute the result (in other words, Catala is not
a lazy language). Therefore, impossible
is not fit to initialise fields of
structures, for example, even if those fields are never used.