Control Flow
Scalus provides control flow constructs for directing program execution in smart contracts. These constructs—if-then-else and pattern matching—are sufficient for most validator logic and compile efficiently to Plutus Core.
If-Then-Else
The if-then-else expression is the primary conditional construct.
import scalus.prelude.{*, given}
compile {
val balance = BigInt(1000)
val threshold = BigInt(500)
// Basic if-then-else
val status = if balance >= threshold then "sufficient" else "insufficient"
// Nested conditionals
val category =
if balance == BigInt(0) then "empty"
else if balance < BigInt(100) then "low"
else if balance < BigInt(1000) then "medium"
else "high"
// Multi-line blocks
val result = if balance > threshold then
val fee = BigInt(10)
balance - fee
else
balance
// Expression result
val doubled = if balance > BigInt(0) then balance * 2 else BigInt(0)
}Key Points:
- Both branches must return the same type
- Can be used as an expression (returns a value)
- Supports nested conditions
Pattern Matching
Pattern matching is a powerful feature for deconstructing and analyzing data structures.
Basic Pattern Matching
enum State:
case Empty
case Active(balance: BigInt)
case Locked(balance: BigInt, until: BigInt)
compile {
val state: State = State.Active(BigInt(1000))
// Match on enum variants
state match
case State.Empty =>
BigInt(0)
case State.Active(balance) =>
balance
case State.Locked(balance, until) =>
if until < currentTime then balance else BigInt(0)
// All cases must be covered or use wildcard
state match
case State.Active(balance) => balance
case _ => BigInt(0) // Wildcard catches all other cases
}Nested Patterns
You can pattern match on nested structures:
case class Token(policy: ByteString, name: ByteString)
case class Balance(token: Token, amount: BigInt)
enum Wallet:
case Empty
case Single(balance: Balance)
case Multiple(balances: List[Balance])
compile {
val wallet: Wallet = Wallet.Single(
Balance(Token(hex"abc", hex"def"), BigInt(1000))
)
// Nested pattern matching
val totalAmount = wallet match
case Wallet.Empty =>
BigInt(0)
case Wallet.Single(Balance(Token(policy, _), amount)) =>
amount // Extract nested fields
case Wallet.Multiple(balances) =>
balances.map(_.amount).foldLeft(BigInt(0))(_ + _)
}Pattern Bindings
Use @ to bind a pattern to a variable while also destructuring it:
case class Account(owner: ByteString, balance: BigInt)
compile {
val state = State.Active(Account(hex"abc", BigInt(1000)))
state match
case State.Active(acc @ Account(owner, balance)) =>
// Both `acc` and individual fields available
if balance > BigInt(500) then acc.balance else BigInt(0)
case _ =>
BigInt(0)
}List Patterns
compile {
val numbers = List(1, 2, 3, 4, 5)
numbers match
case Nil =>
"empty list"
case head :: Nil =>
s"single element: $head"
case first :: second :: rest =>
s"at least two elements: $first and $second"
case _ =>
"other"
}Tuple Patterns
compile {
val pair = (BigInt(10), ByteString.fromHex("abc"))
pair match
case (amount, hash) if amount > BigInt(0) =>
// Note: guards not supported, see workaround below
hash
case _ =>
ByteString.empty
}Pattern Matching with Constants
compile {
val value = BigInt(42)
value match
case BigInt(0) => "zero"
case BigInt(1) => "one"
case _ => "other"
// Boolean patterns
val flag = true
flag match
case true => "yes"
case false => "no"
}Guards (Not Supported - Workarounds)
Pattern matching guards (case pattern if condition =>) are not supported in Scalus. However, there are effective workarounds:
Workaround 1: Move Guard to Right-Hand Side
Instead of:
// NOT SUPPORTED
numbers match
case head :: tail if head > 10 => "big"
case head :: tail if head > 5 => "medium"
case _ => "small"Do this:
// SUPPORTED
numbers match
case head :: tail =>
if head > 10 then "big"
else if head > 5 then "medium"
else "small"
case Nil => "empty"Workaround 2: Extract to Helper Function
def processAccount(acc: Account): BigInt =
if acc.balance > BigInt(1000) then acc.balance * 2
else if acc.balance > BigInt(500) then acc.balance
else BigInt(0)
compile {
state match
case State.Active(account) => processAccount(account)
case _ => BigInt(0)
}Workaround 3: Nested Matching
compile {
state match
case State.Active(account) =>
account.balance match
case b if b > BigInt(1000) => /* Not supported */
// Instead:
case b =>
if b > BigInt(1000) then b * 2
else if b > BigInt(500) then b
else BigInt(0)
case _ => BigInt(0)
}Workaround 4: Filter Then Match
compile {
val numbers = List(1, 2, 3, 4, 5)
// Instead of matching with guards, filter first
val largeNumbers = numbers.filter(_ > 3)
largeNumbers match
case Nil => "no large numbers"
case head :: tail => s"largest: $head"
}Exhaustiveness Checking
Scala’s compiler ensures all cases are covered:
enum Color:
case Red
case Green
case Blue
compile {
val color: Color = Color.Red
// Compiler ensures all cases are handled
color match
case Color.Red => "red"
case Color.Green => "green"
case Color.Blue => "blue"
// No wildcard needed - all cases covered
// Or use wildcard for remaining cases
color match
case Color.Red => "red"
case _ => "not red"
}Error Handling
Using throw
throw terminates execution immediately, compiling to Plutus ERROR:
compile {
def validateAmount(amount: BigInt): BigInt =
if amount <= BigInt(0) then
throw new Exception("Amount must be positive")
else
amount
val validated = validateAmount(BigInt(100)) // OK
// validateAmount(BigInt(-1)) // Throws error
}Error Messages:
- Error messages can be traced using
sir.toUplc(generateErrorTraces = true) - Useful for debugging offchain
- Keep messages concise to reduce script size
Using Option
compile {
def safeDivide(a: BigInt, b: BigInt): Option[BigInt] =
if b == BigInt(0) then None
else Some(a / b)
safeDivide(BigInt(10), BigInt(2)) match
case Some(result) => result
case None => BigInt(0)
}Using Either
compile {
def validateTransfer(from: Account, amount: BigInt): Either[String, Account] =
if amount <= BigInt(0) then
Left("Invalid amount")
else if from.balance < amount then
Left("Insufficient balance")
else
Right(Account(from.owner, from.balance - amount))
validateTransfer(account, BigInt(500)) match
case Right(newAccount) => newAccount.balance
case Left(error) => throw new Exception(error)
}Loops (Not Supported - Use Recursion)
Scalus doesn’t support while or for loops. Use recursion or higher-order functions instead:
Recursion Instead of Loops
compile {
// Sum using recursion
def sum(list: List[BigInt]): BigInt =
list match
case Nil => BigInt(0)
case head :: tail => head + sum(tail)
// Factorial using recursion
def factorial(n: BigInt): BigInt =
if n <= BigInt(1) then BigInt(1)
else n * factorial(n - 1)
// Find element using recursion
def find(list: List[BigInt], target: BigInt): Boolean =
list match
case Nil => false
case head :: tail =>
if head == target then true
else find(tail, target)
}Higher-Order Functions Instead of Loops
compile {
val numbers = List(1, 2, 3, 4, 5)
// Sum - instead of for loop
val sum = numbers.foldLeft(BigInt(0))(_ + _)
// Filter - instead of filtering loop
val evens = numbers.filter(_ % 2 == 0)
// Transform - instead of transformation loop
val doubled = numbers.map(_ * 2)
// Count - instead of counting loop
val countLarge = numbers.filter(_ > 3).length
// All/Any - instead of validation loop
val allPositive = numbers.forall(_ > 0)
val anyLarge = numbers.exists(_ > 10)
}Early Returns (Not Supported)
Scalus doesn’t support early returns. Use pattern matching or conditionals instead:
Instead of Early Return
// NOT SUPPORTED
def process(value: BigInt): BigInt = {
if value < 0 then return 0 // Not supported
if value > 100 then return 100 // Not supported
value * 2
}Use Pattern Matching or Nested Ifs
compile {
def process(value: BigInt): BigInt =
if value < BigInt(0) then BigInt(0)
else if value > BigInt(100) then BigInt(100)
else value * 2
// Or use match
def processWithMatch(value: BigInt): BigInt =
value match
case v if v < BigInt(0) => BigInt(0) // Guard not supported
case v => // Workaround:
if v < BigInt(0) then BigInt(0)
else if v > BigInt(100) then BigInt(100)
else v * 2
}Best Practices
- Prefer pattern matching over nested ifs - More readable and safer
- Always handle all cases - Use wildcard for catch-all
- Keep conditions simple - Complex logic should be extracted to functions
- Use meaningful patterns - Destructure to give names to values
- Avoid deep nesting - Extract to helper functions
- Move guard conditions to RHS - Since guards aren’t supported
- Use recursion judiciously - Be aware of stack depth
- Leverage type system - Let compiler check exhaustiveness
Common Patterns
Validation Pattern
compile {
def validate(input: BigInt): Either[String, BigInt] =
if input < BigInt(0) then
Left("Negative value")
else if input > BigInt(1000000) then
Left("Value too large")
else
Right(input)
validate(BigInt(500)) match
case Right(value) => value
case Left(error) => throw new Exception(error)
}State Machine Pattern
enum State:
case Initial
case Processing(step: BigInt)
case Complete(result: BigInt)
case Failed(reason: String)
compile {
def transition(state: State, input: BigInt): State =
state match
case State.Initial =>
if input > BigInt(0) then State.Processing(BigInt(1))
else State.Failed("Invalid input")
case State.Processing(step) =>
if step >= BigInt(10) then State.Complete(step * input)
else State.Processing(step + 1)
case State.Complete(_) | State.Failed(_) =>
state // Terminal states
}Safe Unwrapping Pattern
compile {
def getBalance(maybeAccount: Option[Account]): BigInt =
maybeAccount match
case Some(account) => account.balance
case None => BigInt(0)
// Or use getOrElse
val balance = maybeAccount.map(_.balance).getOrElse(BigInt(0))
}List Processing Pattern
compile {
def processAll(items: List[BigInt]): BigInt =
items match
case Nil => BigInt(0)
case head :: tail =>
val processed = head * 2
processed + processAll(tail)
// Or use fold
val result = items.foldLeft(BigInt(0))((acc, item) => acc + item * 2)
}