Skip to main content

Posts about aoc

Advent of Code - 2024

Advent of Code 2024 in Zig

This year, I decided to make things interesting and tackle Advent of Code using Zig. Zig is a low-level systems language that aims to be simple, predictable, and efficient. Coming from a Ruby, Python, and Rust background, diving into Zig was a mix of fun, frustration, and some unexpected insights.

What I Liked

Iteration Feels Good 🚀

Zig has a few neat iteration features. For example, for loops allow you to specify an index parameter, which makes enumeration straightforward. Similarly, while loops integrate nicely with the error and optional types, reducing boilerplate. More on that below!

fn printNumbers() void {
    const numbers = [_]i32{1, 2, 3, 4, 5};
    for (numbers, 0..) |num, index| {
        std.debug.print("Index: {}, Value: {}\n", .{index, num});
    }
}

fn whileLoopExample() void {
    var i: i32 = 0;
    while (i < 5) : (i += 1) {
        std.debug.print("Iteration: {}, Value: {}\n", .{i, i * 2});
    }
}

fn iteratorExample(iter: anytype) void {
    // iter.next() returns an Optional value;
    // the loop will terminate when the value is `null`
    // if the value is present, then it's passed in as the captured
    // variable |val| here.
    while (iter.next()) |val| {
        std.debug.print("Value: {}\n", .{val});
    }
}

else Clauses for loops 🔁

A cool feature is the else clause for for and while loops. This lets you handle cases where a loop completes without breaking, which is handy for search algorithms and cleanup logic. Python also supports else clauses in for loops, while Ruby does not. This is something I miss about Python when working in Ruby. Python's else in loops allows for a clean way to handle cases where a loop completes without finding a match, while in Ruby, you often have to rely on find or detect instead.

fn findValue() void {
    const values = [_]i32{1, 2, 3, 4, 5};
    for (values) |v| {
        if (v == 10) {
            return;
        }
    } else {
        std.debug.print("Value not found!\n", .{});
    }
}

Option and Error Union Types ❓⚠️

Zig does not have exceptions, instead relying on option and error union types. I really like this style of programming, which you see much more commonly in Rust. Having to explicitly deal with errors rather than letting exceptions bubble up through the stack makes the code a lot easier to reason about. I like that Zig has built in support for working with options and errors.

Optionals (?T) help represent values that may be absent, and errors (!T) are returned instead of thrown.

You can use orelse to provide a fallback value for an optional expression, which makes handling possibly null values straightforward.

fn getNumber() ?i32 {
    return null;
}

fn example() void {
    const value = getNumber() orelse 42;
    std.debug.print("Number: {}", .{value});
}

Optionals can also be used in an if statement or while loop to conditionally handle the presence of a value:

fn checkOptional() void {
    const maybe_value = getNumber();
    if (maybe_value) |val| {
        std.debug.print("Got a value: {}", .{val});
    } else {
        std.debug.print("No value found!", .{});
    }
}

Similarly, catch is used for handling errors when working with error unions:

fn mightFail() !i32 {
    return error.Failure;
}

fn anotherFunction() !i32 {
    return 42;
}

fn handleErrors() void {
    const result = mightFail() catch |err| {
        std.debug.print("Error occurred: {}", .{err});
        return;
    };
    std.debug.print("Success: {}", .{result});
}

And try is used to unwrap the value from an error union, returning any errors to the caller. This is similar to Rust's ? operator.

fn handleErrorsWithTry() !void {
    // If anotherFunction() returns an error, we return it here
    const result = try anotherFunction();
    std.debug.print("Got result: {}", .{result});
}

Labelled Breaks ⛔

Nested loops are usually annoying, but Zig’s labelled break statements make them way easier to manage. No more tracking weird flags just to escape a loop early.

fn nestedLoop() void {
    outer: for (0..3) |i| {
        for (0..3) |j| {
            if (i == 1 and j == 1) {
                break :outer;
            }
            std.debug.print("{} {}\n", .{i, j});
        }
    }
}

Pointers (Yes, Actually) ➡️

Pointers generally strike fear into the heart of programmers, but in Zig they feel well-structured and easy to use. The explicit handling makes memory management clearer and less error-prone. In Rust, you have to fight against the borrow checker a lot, especially when working with tree structures—which are very common in Advent of Code! Being able to use pointers makes writing these kinds of data structures far less cumbersome... as long as you don't mind debugging a few segfaults.

fn pointerExample() void {
    var x: i32 = 42;
    const ptr: *i32 = &x;
    std.debug.print("Value: {}\n", .{ptr.*});
}

Memory Management 🏗️

Building on the advantages of pointers, I appreciated having more direct control over memory allocations. Zig’s explicit allocator model and the defer keyword make resource management predictable and efficient. Being able to choose and switch allocators as needed, especially leveraging an arena allocator when performance and cleanup efficiency matter, is a huge plus. It’s refreshing to have fine-grained memory control without excessive boilerplate.

Here's an example of using an arena allocator for efficient memory management:

const std = @import("std");

fn example(allocator: std.mem.Allocator) !void {
    var list = std.ArrayList(i32).init(allocator);
    defer list.deinit();

    try list.append(42);
    try list.append(7);

    for (list.items) |item| {
        std.debug.print("{}", .{item});
    }
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    try example(allocator);
}

Annoyances 😬

Zig isn't perfect (yet? 😛). A few things that frustrated me:

  • Verbose Debug Printing: Zig doesn’t have varargs, so you have to use struct literals when passing arguments to std.debug.print. While this makes sense, it can feel tedious to wrap arguments in an anonymous struct .{ ... } all the time.
  • Lack of Functional Programming Constructs: Coming from Ruby and Rust, I missed having functional constructs like map and filter. In Zig, accomplishing similar transformations often requires writing explicit loops, fighting with types, which made writing data transformation code more cumbersome.
  • Cryptic Compiler Errors: Some compiler errors, particularly related to comptime operations like string formatting, can be difficult to trace. The error messages don’t always point to the exact issue in the source code, which makes debugging trickier. Hopefully, this improves in future versions of Zig.

Final Thoughts

Despite the quirks, using Zig for Advent of Code was a fun challenge. It forced me to think differently about problem-solving and get my hands dirty with lower-level programming again. The explicit memory management and structured error handling are great, but I definitely missed the expressiveness of Ruby and Rust.

If you’re curious about my solutions, you can check them out on GitHub. Zig is an interesting language, and while I won’t be using it for everything, it’s definitely worth exploring!

Rust learning resources

For the past 5 years to so, I've been telling myself that I want to learn rust.

And for the past 4 years, I've finished the year doing little to no rust learning :(

This year I've actually made some progress! I wanted to share some of the things that finally helped me get past the learning hump I was struggling with.

Rustlings

Rustlings is a great little project that you run locally. It presents small example programs where you need to fix up some error, or implement some small piece of functionality.

I think that Rustlings helped me more than any other tool to get over the initial learning curve, and get comfortable with the basics of rust syntax.

Exercism

While rustlings gave me a decent foundation for the basics of rust syntax, exercism really has helped build out my knowledge of the standard library. It's a great resource for learning idiomatic ways of solving problems in different programming languages. I really enjoyed trying to solve a problem on my own first, and then looking at other people's solutions after the fact. You almost always learn something by looking at how somebody else has solved the same problem you have.

Exercism has helped me build out my rust "vocabulary" more than any other learning tool so far.

Feel free to check out my profile there!

Advent of Code

Advent of code is an annual set of programming puzzles / challenges. I really look forward to doing these every year, and last year I finally finished completing all the puzzles from all the years. Last year I completed the problems with Ruby, but this year I'm going to try to solve them all with Rust.

I've published most of my solutions for previous years in my adventofcode github repo

The Book

No discussion of rust learning resources would be complete without mentioning The Book, aka The Rust Programming Language book.

I have to admit that I didn't find this terribly useful as an initial resource. Several times I tried to learn rust by starting at the beginning of The Book, and working my way through the various chapters. I never made it very far.

I did find a great channel on YouTube, Let's Get Rusty, which goes over parts of the book in order. Watching Bogdan go through various examples from the book was very helpful.

Learning about learning

What have I learned from this?

I learn best when I have a goal to achieve, and have to make use of my knowledge to achieve the goal. It's hard to learn just by reading about a topic. I think that part of that is because actually trying to write code requires that you've actually internalized some knowledge. It's very humbling to read through some documentation, and then try and put it into practice right away, and struggle to write down the most basic examples of what you've just read :)

What about you? What are some ways you've found to be helpful in learning a new language?