Syntax
Fenl is composed of expressions. Every Fenl expression describes a stream of values. Expressions may be composed to form new expressions.
Constructors
Fenl supports a rich variety of value types. Each type has a corresponding syntax for writing values in Fenl. See the Data Model section for example syntax for constructing different value types.
Unary and Binary operations
Arithmetic operations can be expressed as they are in most languages:
1 + 2 / (3 * 4)
Logical operations use standard comparators, and can be combined with
and
and or
:
true and (5 >= 2) or !false
Data types that compose multiple values allow individual values to be referenced using dot-syntax:
{ a: 10, b: true }.a
All Fenl expressions produce a value, so conditional logic is primarily
used to determine when an expression’s value should be null
, for
example the following expression’s value is null
5 | if(false)
Let Binding
Let-binding is a special operation that introduces local names for other expressions. It is useful when a given expression needs to be referenced multiple times, or when you want to break up a complex expression into smaller pieces.
Let bindings have two components: name / expression bindings followed by a body expression in which the bound names may be referenced.
let the_answer = 42
in the_answer * 2
Multiple bindings may be introduced by repeating the let
portion.
Bindings may reference prior bindings.
let the_answer = 42
let not_the_answer = the_answer * 2
in the_answer != not_the_answer
The value of a let expression is the value of the in
expression,
evaluated in the context of the name bindings.
Function Calls
Functions are called with parens. Function parameters are named.
substring(s = "input", start = 0, end = 2)
Some parameters declare default values. These parameters may be omitted from the function call.
substring("input")
However, if you provide a value for a named parameter with a default value, the parameter name must be included.
substring("input", 0) # invalid syntax
substring("input", start = 0) # valid syntax
Pipe Syntax
In Fenl the pipe operator |
can be used to chain function calls. The
pipe operator is a binary operation that binds the left-hand expression
to the name $input
within the right-hand expression. The pipe
operation lhs | rhs
is equivalent to the let-binding
let $input = lhs in rhs
.
42 | mul(2, $input)
Functions used within the RHS of a pipe expression may omit required
arguments to use $input
. The following is equivalent to the previous
expression.
42 | mul(2)
Pipe operators are useful for applying multiple operations in sequence.
the_answer
| add(10)
| div(2)
| gt(0)
Design
Many functions can be though of having "data" parameters
and "config" parameters. For example Fenl’s functions try to place "data" parameters as the last required / positional parameter. Placing "data" parameters last makes it easy to omit them when using pipe syntax. The resulting code emphases the "config" parameters when reading chains of operations. |
Generally, A | B()
is equivalent to the explicit application
A | B($input)
which is equivalent to B(A)
.
Name Resolution
Fenl name resolution is context-dependent. The context generally starts
with the set of user-defined ref:tableservice[tables] and
ref:viewservice[views]. ref:computeservice_query[Query requests] may
provide additional name bindings with the withTables
parameter.
Names may be added to the context with let
and the |
operator. Names
bound in these ways are only visible in the associated subexpressions,
for example the in
clause of let
and the RHS expression of |
.
Names may be bound more than once. If a name is bound two times, the
second binding is only visible within the in
subexpression of the
second binding.
let current_favorite = "pizza"
let first_favorite = current_favorite
let current_favorite = "hot dogs"
in { current_favorite, first_favorite }
In this example, the name first_favorite
is bound to the value of
current_favorite
at the time it is declared, then the name
current_favorite
is bound to a different value. The value of
first_favorite
is unaffected by the re-binding of current_favorite
.
{ "current_favorite": "hot dogs", "first_favorite": "pizza"}
Syntax Design Principles
Fenl was designed with a few key principles in mind. In alphabetic order they are:
-
Composable: Complex behaviors should be possible by composing simple operations.
-
Consistent: Similar operations should be expressed similarly.
-
Data Centric: Fenl focuses on manipulating data. Failures are data too.
-
Deterministic: Applying the same operations to the same data should produce the same results.
-
Explicit: Explicit syntax more clearly indicates what is happening than implicit behavior. It is easier to add implicit behavior than remove confusing implicit behaviors.
-
Familiar: All factors being equal, Fenl prefers to be familiar. Divergence must have clear benefits and rationale.
-
Flat: Flat syntax is easier to read and understand than nested.
-
Informative: Fenl strives to inform how to think about defining features and guide users to success.
-
Local: Reasoning about behavior should be possible with only the information "nearby". Generally, expressions should be self-contained.
-
Safe & Performant: Features should be safe and performant by default.
-
Simple: Simple operations should be simple to express. Common operations should be simple. Not all conceivable operations are necessary.