Starklings5: Functions in Cairo

Extropy.IO
8 min readSep 6, 2024

We’re continuing our journey with Starklings Cairo, and this time, we’re diving into functions. Functions are a fundamental part of any programming language, and Cairo is no different. They allow us to encapsulate logic, make our code reusable, and keep it organized.

If you’re just joining us, feel free to check out our previous posts on Variables in Cairo , Primitive Types in Cairo.

functions1.cairo

In this exercise, we’re tasked with defining and using a simple function in Cairo. Here’s the starting code:

// functions1.cairo
// Execute `starklings hint functions1` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
call_me();
}

The main function attempts to call another function named call_me, but there's a problem—the call_me function hasn't been defined yet. Let's take a look at the error message:

functions1.cairo Errors

🟡 Running exercises/functions/functions1.cairo exercise...
Compiling exercise_crate v0.1.0 (/Users/desmo/repos/starklings-cairo1/runner-crate/Scarb.toml)
error: Function not found.
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:7:5
call_me();
^*****^
could not compile `exercise_crate` due to previous error
⚠️ Failed to run exercises/functions/functions1.cairo! Please try again.

The error clearly indicates that the compiler can’t find the call_me function. This makes sense because we haven't defined it yet. Let's fix that by defining the function.

functions1.cairo Solution

To solve this, we need to define the call_me function before it's used in main. Here's the updated code:

fn call_me() {
println!("ring, ring");
}
fn main() {
call_me();
}

Explanation

  1. Defining the Function:
  • We define a function named call_me using the fn keyword. This function doesn't take any parameters and doesn't return a value.
  • Inside the function, we use println!("ring, ring"); to print a simple message to the console. This is just a placeholder for any functionality you might want to add later.

2. Calling the Function:

  • In the main function, we callcall_me();, which triggers the execution of the call_me function.
  • This structure is common in programming, where the main function acts as the entry point for the program and other functions are called from there.

functions2.cairo

In the previous exercise, we learned how to define a simple function and call it within our main function. Now, we’re taking it a step further by introducing parameters to our functions. Parameters allow us to pass data into a function, making it more dynamic and reusable. Let’s dive into this exercise.

// functions2.cairo
// Execute `starklings hint functions2` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
call_me(3);
}
fn call_me(num:) {
println!("num is {}", num);
}

functions2.cairo Errors

As usual, let’s start by examining the errors that occur when we try to compile this code.

Running exercises/functions/functions2.cairo exercise...
Compiling exercise_crate v0.1.0 (/Users/desmo/repos/starklings-cairo1/runner-crate/Scarb.toml)
error: Missing tokens. Expected a type expression.
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:10:16
fn call_me(num:) {
^
error: Unknown type.
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:10:16
fn call_me(num:) {
^
error: Type annotations needed. Failed to infer ?3
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo[println_macro][writeln_macro]:4:22
match core::fmt::Display::fmt(__write_macro_arg0__, ref __formatter_for_print_macros__) {
^*****^
could not compile `exercise_crate` due to previous error
⚠️ Failed to run exercises/functions/functions2.cairo! Please try again.

The errors indicate that the call_me function is missing a type annotation for its num parameter. In Cairo, as in many statically-typed languages, every parameter must have an explicit type. Let’s fix that by adding the necessary type annotation.

functions2.cairo Solution

Here’s how we can solve this issue:

fn main() {
call_me(3);
}
fn call_me(num: u8) {
println!("num is {}", num);
}

Explanation

  1. Type Annotation:
  • In the call_me function, we add a type annotation to the num parameter, specifying that it is of type u8 (an unsigned 8-bit integer). This tells the compiler exactly what type of data num should hold.

2. Printing with println!:

  • The println! macro is used to print the value of num to the console. The {} in the string is a placeholder that gets replaced by the value of num at runtime.

Note on i32 in Cairo

You might have tried using an i32 (as it’s the default type in Rust) and noticed that the code didn’t compile. But why can’t we use an i32 type in Cairo as we would in Rust? Here’s what happens if we try:

🟡 Running exercises/functions/functions2.cairo exercise...
Compiling exercise_crate v0.1.0 (/Users/desmo/repos/starklings-cairo1/runner-crate/Scarb.toml)
error: Trait has no implementation in context: core::fmt::Display::<core::integer::i32>
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo[println_macro][writeln_macro]:4:31
match core::fmt::Display::fmt(__write_macro_arg0__, ref __formatter_for_print_macros__) {
^*^
could not compile `exercise_crate` due to previous error

This error occurs because Cairo’s standard library doesn’t yet have an implementation for the Display trait for the i32 type in the println! macro. In Rust, this wouldn’t be an issue because i32 is fully supported and can be easily formatted using println!.

functions3.cairo

In the previous exercises, we explored how to define functions with and without parameters. Now, let’s tackle a situation where our function requires a parameter, but the function call is missing it. This is a common mistake that helps us understand the importance of correctly matching function signatures with their calls.

// functions3.cairo
// Execute `starklings hint functions3` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
call_me();
}
fn call_me(num: u64) {
println!("num is {}", num);
}

functions3.cairo Errors

Let’s start by examining the error that occurs when we try to compile this code:

🟡 Running exercises/functions/functions3.cairo exercise...
Compiling exercise_crate v0.1.0 (/Users/desmo/repos/starklings-cairo1/runner-crate/Scarb.toml)
error: Wrong number of arguments. Expected 1, found: 0
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:7:5
call_me();
^*******^
could not compile `exercise_crate` due to previous error
⚠️ Failed to run exercises/functions/functions3.cairo! Please try again.

The error message indicates that the call_me function expects one argument, but none was provided in the function call. This mismatch causes the code to fail at compile time.

functions3.cairo Solution

To resolve this issue, we need to provide the required argument when calling the call_me function. Here’s the corrected code:

fn main() {
call_me(42);
}
fn call_me(num: u64) {
println!("num is {}", num);
}

Explanation

  1. Function Parameter:
  • The call_me function is defined to accept a single parameter, num, of type u64. This means every time we call call_me, we must provide a u64 value.

2. Providing the Argument:

  • In the main function, we correct the function call by passing the argument 42 to call_me. This value will be passed to the num parameter, allowing the function to execute correctly.

3. Output:

  • The println! macro will now output num is 42, demonstrating that the function has received and correctly handled the argument.

By ensuring that the function call matches the function’s signature, we avoid errors and ensure that our code behaves as expected.

functions4.cairo

In this exercise, we explore function return types and how they are crucial for making our functions work correctly in Cairo. The scenario is a simple one: a store is having a sale where you get a discount based on whether the price is even or odd. This exercise will help us understand how to define functions with return types and why it’s important to ensure those types are correctly specified.

// functions4.cairo
// Execute `starklings hint functions4` or use the `hint` watch subcommand for a hint.
// This store is having a sale where if the price is an even number, you get
// 10 Cairobucks off, but if it's an odd number, it's 3 Cairobucks off.
// (Don't worry about the function bodies themselves, we're only interested
// in the signatures for now. If anything, this is a good way to peek ahead
// to future exercises!)
// I AM NOT DONE
fn main() {
let original_price = 51;
println!("sale_price is {}", sale_price(original_price));
}
fn sale_price(price: u32) -> {
if is_even(price) {
price - 10
} else {
price - 3
}
}
fn is_even(num: u32) -> bool {
num % 2 == 0
}

functions4.cairo Errors

Let’s take a look at the errors that occur when we attempt to compile this code:

🟡 Running exercises/functions/functions4.cairo exercise...
Compiling exercise_crate v0.1.0 (/Users/desmo/repos/starklings-cairo1/runner-crate/Scarb.toml)
error: Missing tokens. Expected a type expression.
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:17:29
fn sale_price(price: u32) -> {
^
error: Type annotations needed. Failed to infer ?4
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo[println_macro][writeln_macro]:4:22
match core::fmt::Display::fmt(__write_macro_arg0__, ref __formatter_for_print_macros__) {
^*****^
error: Unknown type.
--> /Users/desmo/repos/starklings-cairo1/runner-crate/src/lib.cairo:17:30
fn sale_price(price: u32) -> {
^
could not compile `exercise_crate` due to previous error

The main issue here is that the sale_price function is missing its return type. The error message indicates that the compiler expects a type to be specified after the -> symbol in the function signature.

functions4.cairo Solution

To fix this, we need to explicitly declare the return type of the sale_price function. Since the function is returning a modified price, which is of type u32, the return type should also be u32. Here's the corrected code:

fn main() {
let original_price = 51;
println!("sale_price is {}", sale_price(original_price));
}
fn sale_price(price: u32) -> u32 {
if is_even(price) {
price - 10
} else {
price - 3
}
}
fn is_even(num: u32) -> bool {
num % 2 == 0
}

Explanation

  1. Return Type Declaration:
  • The function sale_price now correctly declares that it returns a u32 type. This matches the type of the price parameter, ensuring that the function signature aligns with what the function actually returns.

2. Function Logic:

  • The sale_price function calculates the sale price based on whether the original price is even or odd. It uses the is_even helper function to determine this and applies the appropriate discount.

3. Helper Function:

  • The is_even function remains unchanged and returns a bool value indicating whether a number is even. This function is crucial for the logic inside sale_price.

By specifying the correct return type, we ensure that the function works as intended and that the compiler can correctly infer types throughout the program. This is a fundamental aspect of writing robust and error-free code in statically-typed languages like Cairo.

Conclusion

In this blog, we walked through a series of exercises that highlighted the importance of functions in the Cairo programming language. We started with the basics of defining and calling simple functions and gradually introduced more complexity by adding parameters and return types.

Through these exercises, we learned several key concepts:

  1. Function Signatures: Understanding how to properly define a function’s parameters and return types is crucial for writing clear and maintainable code. Cairo, like many statically-typed languages, requires explicit type annotations, which help prevent errors and make the code more predictable.
  2. Parameter Handling: We saw how functions can become more flexible and reusable when they accept parameters, allowing us to pass in data that the function can process. This is fundamental to writing dynamic code that can handle a variety of inputs.
  3. Return Types: Specifying return types ensures that functions not only perform their tasks but also provide clear outputs that can be used elsewhere in the program. We saw how missing or incorrect return types lead to compilation errors and how fixing these ensures our code behaves as expected.

Stay tuned for more exercises and tutorials as we continue to explore the Cairo programming language and its applications in the world of blockchain and zero-knowledge proofs. Happy coding!

--

--

Extropy.IO

Oxford-based blockchain and zero knowledge consultancy and auditing firm