Functions
Functions are first-class values in Scalus, enabling functional programming patterns essential for smart contract development. Scalus supports named functions (def), anonymous functions (lambdas), and higher-order functions.
Defining Functions
Basic Function Definition
import scalus.prelude.{*, given}
compile {
// Simple function
def add(a: BigInt, b: BigInt): BigInt =
a + b
// Function with explicit return type
def multiply(x: BigInt, y: BigInt): BigInt =
x * y
// Multi-line function
def calculate(a: BigInt, b: BigInt): BigInt =
val sum = a + b
val doubled = sum * 2
doubled + 1
// Call functions
val result = add(BigInt(10), BigInt(20)) // 30
}Type Inference
Return types can often be inferred, but explicit types are recommended for clarity:
compile {
// Type inferred
def double(x: BigInt) = x * 2
// Explicit return type (recommended)
def triple(x: BigInt): BigInt = x * 3
}Lambda Functions (Anonymous Functions)
Lambdas are functions without names, useful for passing as arguments:
compile {
// Lambda syntax
val increment = (x: BigInt) => x + 1
// Lambda with multiple parameters
val add = (a: BigInt, b: BigInt) => a + b
// Lambda with explicit type
val multiply: (BigInt, BigInt) => BigInt = (x, y) => x * y
// Multi-line lambda
val complexCalc = (x: BigInt) => {
val doubled = x * 2
val squared = doubled * doubled
squared + 1
}
// Using lambdas
val result = increment(BigInt(41)) // 42
}Shorthand Syntax
Use underscore for concise lambdas:
compile {
val numbers = List(1, 2, 3, 4, 5)
// Verbose
val doubled = numbers.map((x: BigInt) => x * 2)
// Shorthand
val tripled = numbers.map(_ * 3)
// Multiple underscores represent different arguments
val sum = numbers.foldLeft(BigInt(0))(_ + _)
// Equivalent to: (acc, x) => acc + x
}Higher-Order Functions
Functions that take other functions as parameters or return functions:
compile {
// Function taking a function as parameter
def apply(x: BigInt, f: BigInt => BigInt): BigInt =
f(x)
// Usage
val result = apply(BigInt(10), x => x * 2) // 20
// Function returning a function
def makeMultiplier(factor: BigInt): BigInt => BigInt =
(x: BigInt) => x * factor
val double = makeMultiplier(BigInt(2))
val triple = makeMultiplier(BigInt(3))
double(BigInt(10)) // 20
triple(BigInt(10)) // 30
}Common Higher-Order Functions
compile {
val numbers = List(1, 2, 3, 4, 5)
// map: transform each element
val doubled = numbers.map(_ * 2) // [2, 4, 6, 8, 10]
// filter: select elements matching condition
val evens = numbers.filter(_ % 2 == 0) // [2, 4]
// foldLeft: reduce from left
val sum = numbers.foldLeft(BigInt(0))(_ + _) // 15
// foldRight: reduce from right
val product = numbers.foldRight(BigInt(1))(_ * _) // 120
// find: first element matching condition
val firstEven = numbers.find(_ % 2 == 0) // Some(2)
// exists: check if any element matches
val hasLarge = numbers.exists(_ > 10) // false
// forall: check if all elements match
val allPositive = numbers.forall(_ > 0) // true
}Recursive Functions
Recursion is the primary iteration mechanism in Scalus:
compile {
// Simple recursion
def factorial(n: BigInt): BigInt =
if n <= BigInt(1) then BigInt(1)
else n * factorial(n - 1)
// Tail recursion (more efficient)
def factorialTail(n: BigInt, acc: BigInt = BigInt(1)): BigInt =
if n <= BigInt(1) then acc
else factorialTail(n - 1, n * acc)
// List recursion
def sum(list: List[BigInt]): BigInt =
list match
case Nil => BigInt(0)
case head :: tail => head + sum(tail)
// Finding in list
def contains(list: List[BigInt], target: BigInt): Boolean =
list match
case Nil => false
case head :: tail =>
if head == target then true
else contains(tail, target)
}Important:
- Scalus supports recursive functions
- Consider tail recursion for efficiency
- Be aware of recursion depth limits
- Use higher-order functions when possible
Default Parameters
Scalus supports default parameter values:
def greet(name: String, greeting: String = "Hello"): String =
appendString(name, greeting)Named Arguments
Scalus supports named arguments:
def transfer(from: Account, to: Account, amount: BigInt): Transaction =
???
// This works as expected:
transfer(from = alice, to = bob, amount = BigInt(100))Variable Arguments (Varargs)
Scalus supports variable argument lists (vargs) with a little caveat:
def sum(numbers: BigInt*): BigInt =
numbers.list.foldLeft(BigInt(0))(_ + _)You must convert a sequence to a List using scalus.prelude.list extension method
before performing list operations.
Function Overloading
NOT SUPPORTED - Scalus doesn’t support function overloading (multiple functions with the same name):
// NOT SUPPORTED
def add(a: BigInt, b: BigInt): BigInt = a + b
def add(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + cWorkaround: Use different function names or a single function with List:
compile {
// Different names
def add2(a: BigInt, b: BigInt): BigInt = a + b
def add3(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c
// Or use a List
def addAll(numbers: List[BigInt]): BigInt =
numbers.foldLeft(BigInt(0))(_ + _)
addAll(List(1, 2)) // 3
addAll(List(1, 2, 3)) // 6
addAll(List(1, 2, 3, 4)) // 10
}Mutually Recursive Functions
NOT SUPPORTED - Functions cannot call each other recursively:
// NOT SUPPORTED
def isEven(n: BigInt): Boolean =
if n == BigInt(0) then true
else isOdd(n - 1)
def isOdd(n: BigInt): Boolean =
if n == BigInt(0) then false
else isEven(n - 1)Workaround: Combine into a single function or use a helper enum:
compile {
// Combine into one function
def isEven(n: BigInt): Boolean =
(n % BigInt(2)) == BigInt(0)
def isOdd(n: BigInt): Boolean =
!isEven(n)
// Or use helper data structure
enum Parity:
case Even
case Odd
def checkParity(n: BigInt, current: Parity): Boolean =
if n == BigInt(0) then
current match
case Parity.Even => true
case Parity.Odd => false
else
val nextParity = current match
case Parity.Even => Parity.Odd
case Parity.Odd => Parity.Even
checkParity(n - 1, nextParity)
checkParity(BigInt(10), Parity.Even) // true (10 is even)
}Closures
Lambdas can capture variables from their enclosing scope:
compile {
val multiplier = BigInt(10)
// Lambda captures 'multiplier'
val multiplyBy10 = (x: BigInt) => x * multiplier
multiplyBy10(BigInt(5)) // 50
// Function returning closure
def makeAdder(x: BigInt): BigInt => BigInt =
(y: BigInt) => x + y
val add5 = makeAdder(BigInt(5))
add5(BigInt(10)) // 15
}Partial Application
Create new functions by fixing some arguments:
compile {
def add3(a: BigInt, b: BigInt, c: BigInt): BigInt =
a + b + c
// Create a partially applied function
def addWith10And20(c: BigInt): BigInt =
add3(10, 20, c)
addWith10And20(30) // 60
// Using closures for partial application
def partial2(f: (BigInt, BigInt, BigInt) => BigInt, a: BigInt): (BigInt, BigInt) => BigInt =
(b: BigInt, c: BigInt) => f(a, b, c)
val addWith10 = partial2(add3, BigInt(10))
addWith10(20, 30) // 60
}Function Composition
Combine functions to create new ones:
compile {
def double(x: BigInt): BigInt = x * 2
def increment(x: BigInt): BigInt = x + 1
// Manual composition
def doubleAndIncrement(x: BigInt): BigInt =
increment(double(x))
doubleAndIncrement(5) // 11
// Composition helper
def compose[A, B, C](f: B => C, g: A => B): A => C =
(x: A) => f(g(x))
val composed = compose(increment, double)
composed(5) // 11
}Best Practices
- Use descriptive names - Function names should clearly indicate purpose
- Keep functions small - Single responsibility principle
- Prefer immutability - Don’t modify parameters, return new values
- Use type annotations - Explicit return types improve clarity
- Leverage higher-order functions - More concise than explicit recursion
- Avoid deep recursion - Be mindful of stack depth
- Use tail recursion - More efficient for deep recursion
- Document complex functions - Add comments for non-obvious logic
Common Patterns
Validation Function
compile {
def validate(amount: BigInt): Either[String, BigInt] =
if amount < BigInt(0) then
Left("Amount cannot be negative")
else if amount > BigInt(1000000) then
Left("Amount too large")
else
Right(amount)
validate(BigInt(500)) match
case Right(value) => value
case Left(error) => throw new Exception(error)
}Transformation Pipeline
compile {
val numbers = List(1, 2, 3, 4, 5)
val result = numbers
.filter(_ % 2 == 0) // Keep evens
.map(_ * 2) // Double each
.foldLeft(BigInt(0))(_ + _) // Sum all
// result: 12 (2*2 + 4*2 = 4 + 8 = 12)
}Conditional Execution
compile {
def conditionalExecute(
condition: Boolean,
onTrue: () => BigInt,
onFalse: () => BigInt
): BigInt =
if condition then onTrue() else onFalse()
conditionalExecute(
balance > BigInt(1000),
() => processLargeBalance(balance),
() => processSmallBalance(balance)
)
}Memoization Pattern (Using Map)
compile {
// Simple memoization using a map
def fibonacci(n: BigInt, memo: Map[BigInt, BigInt]): (BigInt, Map[BigInt, BigInt]) =
if n <= BigInt(1) then
(n, memo)
else
memo.get(n) match
case Some(result) =>
(result, memo)
case None =>
val (fib1, memo1) = fibonacci(n - 1, memo)
val (fib2, memo2) = fibonacci(n - 2, memo1)
val result = fib1 + fib2
(result, memo2 + (n -> result))
val (result, _) = fibonacci(BigInt(10), Map.empty)
}Error Handling Wrapper
compile {
def tryExecute[A](f: () => A, onError: () => A): A =
try f()
catch
case _: Exception => onError()
val result = tryExecute(
() => riskyOperation(),
() => fallbackValue
)
}Performance Considerations
- Function calls have overhead - Consider inlining small functions
- Recursion depth - Stack depth is limited, use iteration (fold) when possible
- Closure captures - Capturing variables adds memory overhead
- Tail recursion - More efficient than general recursion
- Higher-order functions - May be more expensive than direct code
Using inline
Mark functions as inline to have them expanded at compile time:
compile {
// Inline function - no call overhead
inline def square(x: BigInt): BigInt = x * x
// Usage - will be expanded to: val result = 5 * 5
val result = square(BigInt(5))
}Benefits:
- Zero function call overhead
- Can enable further optimizations
- Useful for small, frequently called functions
Use inline for:
- Simple calculations
- Frequently called helpers
- Performance-critical code
Summary
- Functions are first-class values
- Lambdas provide concise function syntax
- Higher-order functions enable functional patterns
- Recursion replaces loops
- No default parameters, named arguments, varargs, or overloading
- No mutually recursive functions
- Use
inlinefor performance-critical small functions