Sotolf's thoughts and oddities

Odin

I was thinking I would be giving odin a try, it’s one of those new hot languages that are vying for c programmers that wants something a bit extra.

So I fire up an hello world and get rolling

1package main
2
3import "core:fmt"
4
5main :: proc() {
6    fmt.println("Hello, world!)
7}

This is okay enough, syntax is a bit weird but that’s just something kind of subjective. So we soldier along.

This is working on the advent of code problem https://adventofcode.com/2021/day/11

First things first, we need to import data, luckily our file format is very simple this time, just a 10x10 grid of integers.

I found no documentation of how to parse integers after looking around, so I just did the old ascii code - 48 trick:

 1get_data :: proc(filepath: string) -> (result: [10][10]int, ok: bool) {
 2    data := os.read_entire_file(filepath, context.allocator) or_return
 3    defer delete(data, context.allocator)
 4    
 5    row := 0
 6    col := 0
 7
 8    it := string(data)
 9    for rune in it {
10        if rune == '\n' {
11            row += 1
12            col = 0
13            continue
14        } else {
15            result[row][col] = int(rune) - 48
16            col += 1
17        }
18    }
19    return
20}

This is pretty nice otherwise, pretty much feels kind of like go with an a bit more comfortable way to just foreward errors, everything else feels pretty standard

Then we start working on the problem itself first with a higher level of what we want to do. It’s nice that all function arguements are immutable as a default, it’s something I really like to see.

 1part1 :: proc(in_grid: [10][10]int) {
 2    grid := to_octo_grid(in_grid)
 3    flashed := 0
 4
 5    for _ in 0..<100 {
 6        flashed += do_step(&grid)
 7    }
 8
 9    fmt.printf("Part1: %v\n", flashed)
10}

I figured we needed to save a bit more data on each of the octopuses, so we introduce a struct which we can use to save some flashing info, this is pretty nice, I would have really enjoyed to be able to use a struct as a namespace like in zig so that I would be able to have a bit more comfort, but the struct in itself is very straight forward.

1Octo :: struct {
2    val: int,
3    flash: bool,
4}

We want to do the step, so we have another middle layer function that more or less describes the steps, which we implement afterwards:

1do_step :: proc(grid: ^[10][10]Octo) -> int {
2    inc(grid)
3
4    for has_flashing(grid^) {
5        execute_flash(grid)
6    }
7    return finalize_step(grid)
8}

Now some of the places where we would really like to have some comfort comes out, it’s pretty decent in this little test since, we don’t have that many functions and we’re not having to deal with naming that much, but it feels like something that could become an issue with bigger stuff.

 1inc :: proc(grid: ^[10][10]Octo) {
 2    for row in 0..<10 {
 3        for col in 0..<10 {
 4            grid[row][col].val += 1
 5            
 6            if grid[row][col].val == 10 {
 7                grid[row][col].flash = true
 8            }
 9        }
10    }
11}

There are some things that I don’t like here, I don’t really have a nice way to create a mutable iterator, so we will see this double for construction a lot.

Also since I’m unsure about how the values works there is also a lot of using the grid[row][col] construction.

This is basically setting up the grid for the flashing steps where we will keep on going until we’ve had no more flashing happening.

 1has_flashing :: proc(grid: [10][10]Octo) -> bool {
 2    for row in 0..<10 {
 3        for col in 0..<10 {
 4            if grid[row][col].flash {
 5                return true
 6            }
 7        }
 8    }
 9    return false
10}

So to check if we are finished we have to check the grid to see if something is still needing to flash. so here we iterate again..

 1execute_flash :: proc(grid: ^[10][10]Octo) {
 2    for row in 0..<10 {
 3        for col in 0..<10 {
 4            if grid[row][col].flash {
 5                increase_nbrs(grid, row, col)                
 6                grid[row][col].flash = false
 7            }
 8        }
 9    }
10}

And to do the flashing itself, we have to do the same iteration to deal with things that need to flash. So yet another iteration through our grid.

To evade the pyramid of death I chose to factor out dealing with the neighbouring octopuses, because it was starting to get ridiculous

 1increase_nbrs :: proc(grid: ^[10][10]Octo, inrow, incol: int) {
 2    for row in inrow-1..=inrow+1 {
 3        if row >= 0 && row <= 9 {
 4            for col in incol-1..=incol+1 {
 5                if col >= 0 && col <= 9 {
 6                    grid[row][col].val += 1
 7                    if grid[row][col].val == 10 {
 8                        grid[row][col].flash = true
 9                    }
10                }
11            }
12        }
13    }
14}

Here it would be really nice with some iterators, and some contains things but I don’t think that’s not something odin is really out after, it seems like one of those langauges that really doesn’t want to have much hidden stuff but then again, it might be that it does have it, I just don’t manage to find anything in the documentation, because it’s very rudimentary.

 1finalize_step :: proc(grid: ^[10][10]Octo) -> int {
 2    count := 0
 3
 4    for row in 0..<10 {
 5        for col in 0..<10 {
 6            if grid[row][col].val >= 10 {
 7                grid[row][col].val = 0
 8                count += 1
 9            }
10        }
11    }
12    
13    return count
14}

Then we just fix up the end of the step and count the amount of octopi that was flashing, easy peasy, but yet another double for loop..

So that was basically it for the first part, for the second part we were lucky and could basically just reuse the functions we made for part 1

 1part2 :: proc(in_grid: [10][10]int) {
 2    grid := to_octo_grid(in_grid)
 3    flashed := 1
 4
 5    for {
 6        cur := do_step(&grid)
 7        if cur == 100 {
 8            break
 9        }
10        flashed += 1
11    }
12
13    fmt.printf("Part2: %v\n", flashed)
14}

Nothing really interesting to see here.

For the complete listing of the code, it can be found here

Thoughts at the end.

Odin does some things pretty well, it’s reasonably fun, mostly it reminds me of go with just a bit less boilerplate, and a lot worse documentation. It some times got the the point that to figure out how something worked, I looked up how it worked in go, and just used it like that.

The named returns are pretty nice, and makes function feel quite a lot cleaner.

Most things are rather straight foreward, and things just works, but I do find it a bit hard to feel exited about it. Again, the biggest bugbear for me is the documentation, it’s hard to really build up a feel for the language, when you’re starting from more or less scratch, and the only real examples you have are implementations in the stdlib.

Will I use odin again, maybe, but I will wait until it has documentation that is nicer to read, the language is decent, but I don’t know, it just doesn’t feel as great to write in, I can do things way more easier, with nim, and it’s documentation is better. I think I also prefer zig to odin, even though, odin was a less pain in the ass about allocations. I just felt a lot better about my zig code, and it felt better to write.