Functions in Crystal
Learn how to define and call functions in Crystal - parameters, default and named arguments, scope, recursion, and higher-order functions with blocks and procs
Functions are how you package logic into reusable, named units. In Crystal, the keyword is def, and the syntax will feel instantly familiar to anyone who has written Ruby. But Crystal is a compiled, statically typed language, so functions carry extra power: the compiler can infer parameter and return types, catch type errors before your program ever runs, and still let you add explicit annotations when you want clarity or documentation.
Because Crystal is multi-paradigm - object-oriented, functional, and concurrent all at once - “functions” wear several hats. A bare def at the top level acts like a standalone function, while a def inside a class becomes a method. Crystal also embraces functional ideas: blocks, Proc objects, and a rich set of higher-order collection methods like map and select are everyday tools rather than advanced curiosities.
In this tutorial you’ll learn how to define functions, pass parameters with default and named arguments, understand local versus global scope, write recursive functions, and work with higher-order functions. Every example is runnable with Docker - no local Crystal install required.
Defining and Calling Functions
A function is defined with def, a name, an optional parameter list, and an end. Crystal returns the value of the last expression automatically, so an explicit return is usually optional. Type annotations are also optional thanks to global type inference, but adding them documents intent and helps the compiler give better error messages.
Create a file named functions.cr:
| |
Notice that greet has no return keyword - the string literal is the last expression, so it becomes the result. The absolute function shows Crystal’s expressive one-line if modifier for an early return.
Default and Named Arguments
Crystal supports default parameter values, so callers can omit arguments that have sensible defaults. It also supports named arguments, which make calls self-documenting and let you skip over earlier defaults without passing placeholder values.
Create a file named default_named_args.cr:
| |
The last two calls demonstrate the real advantage of named arguments: format_price(100.0, decimals: 0) keeps the default currency while overriding only decimals, with no need to repeat "USD" in the call.
Variable Scope
Variables created inside a function are local to that function - they cannot be seen from the outside, and each call gets its own fresh copies. For values that need to be shared everywhere, Crystal uses constants, which start with an uppercase letter and are visible across the whole program.
Create a file named scope.cr:
| |
Crystal favors local scope and constants over mutable global state. The commented-out puts subtotal line would fail to compile, which is exactly the kind of mistake the compiler catches for you.
Recursion
A recursive function calls itself, breaking a problem into smaller versions of the same problem until it reaches a base case. Crystal handles recursion naturally, and the static type system ensures the parameter and return types line up at every level of the call.
Create a file named recursion.cr:
| |
Every recursive function needs a base case - here, n <= 1 for factorial and n < 2 for Fibonacci - otherwise it would recurse forever.
Higher-Order Functions: Blocks and Procs
This is where Crystal’s functional side shines. Functions can accept blocks of code via yield, accept Proc objects as ordinary parameters, and pass functions around as first-class values. The standard library leans on this heavily: collection methods like map and select take a block and apply it to each element.
Create a file named higher_order.cr:
| |
The repeat method shows the block protocol: yield i runs the block passed at the call site, handing it the current index. The apply_twice method shows the Proc protocol: Int32 -> Int32 is the type of “a function from Int32 to Int32”, and .call invokes it. Both styles let you parameterize behavior, not just data.
Running with Docker
You can run every example above with the official Crystal image - no local installation needed.
| |
Expected Output
Running functions.cr:
Hello, Crystal!
7
42
15
Running default_named_args.cr:
25
1024
USD 19.95
EUR 8.5
USD 100.0
Running scope.cr:
32.4
54.0
Running recursion.cr:
120
55
Running higher_order.cr:
Iteration 0
Iteration 1
Iteration 2
20
2
4
1
4
9
16
25
Key Concepts
defdefines functions and methods - the same keyword is used at the top level (a standalone function) and inside a class (a method).- Implicit return - a function returns the value of its last expression, so
returnis usually optional; use it for early exits like guard clauses. - Optional type annotations - global type inference means types are often inferred, but explicit annotations document intent and improve compiler error messages.
- Default and named arguments - parameters can have default values, and named arguments let callers override only the arguments they care about while keeping other defaults.
- Local scope by default - variables defined in a function are private to that call; share values through uppercase constants rather than globals.
- Recursion needs a base case - a function that calls itself must have a stopping condition, or it will recurse indefinitely.
- Functions are first-class - blocks (via
yield) andProcobjects (Int32 -> Int32) let you pass behavior around, powering higher-order methods likemapandselect.
Running Today
All examples can be run using Docker:
docker pull crystallang/crystal:1.14.0
Comments
Loading comments...
Leave a Comment