Statements - Manhunter07/MFL GitHub Wiki

Like most other (higher) programming languages, MFL supports a small set of statements to make coding easier and faster, while still sticking with the conventions of functional coding. These include branching capabilities, anonymous in-line functions and others.

It is important to note here that some statements can often not be nested and always run to the end of the expression unless terminated by a terminating token like ), ], } or ). In order to avoid problems like these and also to improve readability, it is recommended to put them all in round parentheses. This will not affect their functionality, but will for sure avoid problems with nesting. It also allows the insertion of additional code afterwards.

Conditional branching

Like in most languages, conditional branching is eithr done by if or by case statements. They fulfill similar goals, but focus on a different use case to offer the most readable and shortest way of writing code. Below, you will find a summary of each statement with examples of how they are used.

Binary branching

See also: Boolean evaluation

The keyword if notes a simple binary branch, followed by an expression that evaluates to (non-)emptyness. If the value here is not empty, it is treated as true. Otherwise, it is treated as false. Note that the value does not need to be exactly that of the True constant here, but can be anything that's not 0, "", [] or {}.

The condition is terminated by the keyword then which introduces the value that's evaluated if the condition returns a true-like value. Otherwise, the value here is not evaluated and possible exceptions are not thrown. The keyword then terminates the true value and introduces the false value. That goes to the end of the term, that can either be the end of the whole expression or a terminating token like a parentheses, bracket, brace or a comma. If the condition returns a false-like value on evaluation, the false value is returned. It is however only evaluated itself if the condition is evaluated to false.

Because only one of the two return values is evaluated (either the true or the false value), this statement can and should be used for recursive function calls. This is especially important since MFL does not allow function overloading. if-then-else statements can be used anywhere in your code where direct values or function calls are possible.

Non-binary branching

In many situations it is necessary to next multiple comparisons into each other's else section. This not requires some additional writing, but it also makes code more prone to errors and much harder to read. Because of this, many languages have included a mechanism to define so-called jump labels or case statements (called switch-case in C languages). Those simplify the writing of multiple concatenated if comparisons by omitting the operator and simplifying the overall syntax.

There is no explicit comparison operator, the compiler instead chooses which one to use. In MFL, this will always be the equality comparison operator ?=. Thereby, you cannot use the case statement with value types that do not support equality comparison.

A non-binary branch is introduced by the case keyword, followed by a value (direct or a pre-declared identifier) which will be evaluated once. After the obligatory of keyword you enter a list of case branches that will be checked, and a final else default branch for the case that no branch was valid. This is because in MFL as a functional programming language, each statement must have a return value. Between the case-of section and the else branch there can be an indefinite amount (but at least one) of labels that define branches. Once once of them matches the input value, it is returned and the remaining branches are ignored.

Each branch except for the else branch consists of a value for comparison and a then value that is used if the branch is taken. If not, this value will not be evaluated. case-of-else statements can be used anywhere in your code where direct values or function calls are possible.

Examples

Here we prevent a zero division by ensuring the divisor does not equal zero:

1 / if V then V else 0.01

In this example we want to return a string with either "Empty" or "Not empty", depending on whether or not the Value parameter is an empty value:

function CheckEmpty(Value) = if Value then "Not empty" else "Empty"

In the case of a recursive function call we want to prevent an endless stack call by ensuring we have reached the end of an input array:

function AddAll(Values: array, StartIndex: PosInt = 0, Offset = 0) = Get(Values, StartIndex) + if Asc(StartIndex, Length(Values) - 1) then AddAll(Values, StartIndex + 1, Offset) else Offset

We can write any case statement as an if statement, but it often makes code shorter and more readable if case-of-else is being used:

case x of 0 then "zero", 1 then "one", 2 then "two" else "other"
\ ...is identical with... \
if x ?= 0 then "zero" else (if x ?= 1 then "one" else (if x ?= 2 then "two" else "other"))

Exceptional branching

There is a way to bypass runtime errors and introduce a basic way of exception handling using the keyword try. It is followed by some error-prone code of which you expect to potentially throw an exception. This can be either a built-in exception like a call to undeclared identifier or a user error thrown by your own functions or third-party libraries using functions like Error.

After we defined the default expression we use the except keyword to introduce the exceptional code. This is the part of the statement that is evaluated (only) if the default branch failed. In the case of no exception, only the try part is evaluated. If an error ocurs there, evaluation stops and the except part is evaluated as well. Errors there are however not caught and will be thrown when encountered. The (partial) original return value will be dismissed in the case the exceptional part is used.

try-except statements can be used anywhere in your code where direct values or function calls are possible.

Examples

If the identifier V has not been declared, we simply return 0:

try V except 0

If we cannot ensure a value supports an operation we can show our own error message:

function Sqr(X) = try X ^ 2 except Error("Argument must support exponentiation")

Argument spreading

You can spread an array of values uppon function parameters and use them as arguments. Use the spread keyword for that, followed by a value. The spreaded value must be an array. Only one array can be spread per argument, but you can either concat arrays or spread arrays for different arguments. This is especially useful for (built-in) vararg functions, but can be used in any function available.

Examples

We can call a function with an array of arguments:

Min(spread Values)

Argument spreading also works for function reference calls:

function Call(F: Ref, Args: array) = #F(spread Args)

Additionally, we can concatenate arrays or use functions on them, as we would in other ocasions:

Avg(spread Map(V, func X ret X / 2) + [4, 2.156, 49.9])

We can still add other spreaded or non-spreaded arguments to the function call that are delimeted by comma:

Max(0.016, spread Values1, spread Values2, Pi, 4.5)

The last example would be equal to:

Max(spread [0.016] + Values1 + Values2 + [Pi, 4.5])

Anonymous functions

See also: Functions and Function parameters

Anonymous functions are in-line functions that do not have a name and therefore are disposed when not used anymore. They are not pre-declared and therefore cannot be used afterwards unless stored in a constant or variable, or returned by a function. They are created using func followed by its parameters and ret afterwards, followed by its return value expression.

func-ret statements can be used anywhere in your code where direct values or function calls are possible. They return a reference to an unnamed (anonymous) function object and support the same set of features as a normal function. Anonymous and named functions are fully compatible with each other, there is no difference in read or write compatibility.

Examples

If an anonymous function is stored in a var or constant, it can later be used and called:

const MyFunc = func X, Y ret X + Y

Anonymous functions support parameter constraints and default values, just like normal functions do:

func A & B: Number, C: Number = 0 ret A + B / C