Conditionals, let and pipelines
This page shows how to make decisions with if and case, name intermediate results with let, and chain
function calls with pipelines. It ends with how Elci evaluates your code, which matters for both
correctness and cost on-chain.
if … then … else
Section titled “if … then … else”if is an expression, so it always has an else and both branches have the same type. Chain conditions
with else if:
module Main exposing (main)
feeFor : Int -> IntfeeFor amount = if amount < 1000 then 0
else if amount < 100000 then 10
else amount // 1000
main : Intmain = feeFor 500 + feeFor 5000 + feeFor 250000With --builtins, elci eval also lists every builtin operation the script performed:
elci eval . --builtins260cpu 1489307, mem 5910builtin: LessThanInteger(500, 1000) → Truebuiltin: LessThanInteger(5000, 1000) → Falsebuiltin: LessThanInteger(5000, 100000) → Truebuiltin: AddInteger(0, 10) → 10builtin: LessThanInteger(250000, 1000) → Falsebuiltin: LessThanInteger(250000, 100000) → Falsebuiltin: QuotientInteger(250000, 1000) → 250builtin: AddInteger(10, 250) → 260Each call only tested the conditions it needed: feeFor 500 stopped after the first comparison, and only
feeFor 250000 divided.
case … of
Section titled “case … of”case compares a value against patterns, top to bottom, and evaluates the first branch that matches. _
matches anything:
module Main exposing (main)
label : Int -> Stringlabel count = case count of 0 -> "none"
1 -> "one"
_ -> "many"
main : ( String, String, String )main = ( label 0, label 1, label 7 )( "none", "one", "many" )cpu 1894636, mem 8277Patterns can be Int, String or Bool literals, names, _, tuples, records, lists and constructors of
custom types. A case must cover every possible value; without the _ branch above, the compiler rejects
it. Custom types and pattern matching goes further.
let … in
Section titled “let … in”let names intermediate values. The names are visible in the rest of the let and in the expression after
in:
module Main exposing (main)
firstShare : Int -> Int -> IntfirstShare total shares = let perShare = total // shares
leftover = remainderBy shares total in perShare + leftover
main : Intmain = firstShare 1000 3334cpu 813990, mem 2904A let can also define local functions, optionally with a signature, and destructure tuples and records.
You’ll see those forms on the next pages.
Pipelines and composition
Section titled “Pipelines and composition”Nested calls read inside out. The pipe operator |> passes a value into the next function, so the steps
read top to bottom:
module Main exposing (main)
main : Intmain = [ 5, -3, 12, 0, 8 ] |> List.filter (\n -> n > 0) |> List.map (\n -> n * 2) |> List.foldl (\n total -> n + total) 050cpu 12396014, mem 60117x |> f means f x, and f <| x also means f x, which saves a pair of parentheses. >> and << compose
two functions into a new one: (f >> g) x is g (f x), and (g << f) x is the same.
module Main exposing (main)
double : Int -> Intdouble n = n * 2
isLarge : Int -> BoolisLarge n = n > 10
main : Boolmain = let doubledIsLarge = double >> isLarge in doubledIsLarge 6 && (not <| doubledIsLarge 5)Truecpu 1197680, mem 5906<| and |> have the lowest precedence of all operators, which is why not <| doubledIsLarge 5 needs
parentheses when it’s part of a larger expression.
Strict evaluation
Section titled “Strict evaluation”Elci evaluates eagerly, like Elm. Knowing exactly what gets evaluated matters on-chain, for two reasons: every step costs budget, and a single failure rejects the whole transaction.
Local values are always evaluated, even when unused. This program fails, although it never uses
unused:
module Main exposing (main)
main : Intmain = let unused = 1 // 0 in 5evaluation failed: QuotientInteger: division by zerocpu 276441, mem 1001Debug.trace message value returns value and records message, and elci eval prints each message on a
trace: line, so you can watch the order. Debug, like List, is imported into every module. Both values
are computed here, in source order, although the if only returns one of them:
module Main exposing (main)
main : Intmain = let small = Debug.trace "computing small" 1
large = Debug.trace "computing large" 1000 in if True then small
else large1cpu 471096, mem 2364trace: computing smalltrace: computing largeif, case, && and || evaluate only what they need. Only the chosen branch of an if or case
runs, and && and || skip their right side when the left side decides the answer. So put expensive or
failing work inside a branch:
module Main exposing (main)
safeDivide : Int -> Int -> IntsafeDivide amount parts = if parts == 0 then 0
else amount // parts
main : Intmain = safeDivide 10 0 + safeDivide 10 33cpu 962315, mem 4005The other rules follow the same idea:
- A function call evaluates the function, then its arguments from left to right, before the body runs.
So
Maybe.withDefault fallback mevaluatesfallbackeven whenmis aJust, and(&&) a b, the operator used as a function, evaluatesbeven whenaisFalse. - A partial application such as
add (10 // 0)evaluates its argument immediately. - In a validator, top-level values the handlers use are evaluated once per script run, before the handler is chosen. A failing top-level value therefore fails every purpose of the script.
Debug.trace also costs budget on-chain, so remove traces before you deploy. Debugging and
budgets has more.
Records and tuples groups values into structures.