dwyl / dwyl/learn-zig

Comparison Zig, Elixir, Node on a simple matrix rotation algorithm

Open
#3 0 comments 3 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Homework today: I have a square matrix of numbers and want to rotate it counter-clockwise.

We use `Zig`, `Elixir` and `Node.js` for comparison.

The algorithm is:
- transform the array of strings into an array of arrays
- loop through the elements of this matrix and return the permuted element: `res[dim-1-j][i]=input[i][j]`)

The (unsurprising) learning is that higher level languages manage memory for us.
With `Zig`,
- since you handle dynamic arrays, you have to be careful with the memory handling,
- but also how you use arguments (values or pointers),
- and the type you use (**not** straightforward).

With `Elixir`, we just return the result from a function, no instantiation. Easy peazy (once you found `Enum.at`...).

With `JS`, you need to instantiate a dummy result array as we mutate it. Pretty straightforward once you understood the double loop and the indices combination.

The main difficulty is that `Zig` has version `0.13`, meaning it is not stable. Unless you already know which function you can use, it is really difficult to discover things.
Errors can make you really frustrated too.

## Example

The matrix is given an array of strings made of space separated numbers.

```
["1 2 3", "4 5 6", "7 8 9"]
```

We want to rotate it counter-clockwise (the last column will become the first row). We obtain.

```
[ "3 6 9", "2 5 8", "1 4 7" ]

# or Zig result:
{ { 3, 6, 9 }, { 2, 5, 8 }, { 1, 4, 7 } }
```

No surprise, `Zig` is the fastest, then `JS`, then `Elixir`.

## Zig code

We need to handle the memory needed to build a matrix. We use the `ArenaAllocator` for its simplicity.

Once you are done, you simply `deinit`.

The input array of strings is known at **compile time**.

We use `try` since the memory allocation can fail, thus the main function can return an error (thus `!void`).

We pass a **reference** to this array in a `rotateArray` function, and the allocator.

This function can fail and return a matrix of short unsigned integers (thus `![][]u8`). This is why you use `try`.

We build a slice and allocate memory for it with the allocator we passed.
We then instantiate and allocate memory of each element of our slice, which will be a slice as well.

Then we loop over each row with a `for` loop where we use a reference to the row array and the index.
Then we extract we characters separated by a whitespace with (`std.mem.splitScalar`).
This gives us an `iterator`. With this, we can run a `while` loop as long as we have a `iterator.next`.
We parse the character into a short unsigned integer.
Then we can assign the matrix slice.

Zig code

```zig
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;

/// Parameters:
/// - M: A slice of string slices, each representing a row of the input matrix. The slice is already a pointer
/// - allocator: The allocator to use for memory allocation
///
/// Returns: A 2D slice ([][]u8) representing the rotated matrix
///
/// Error: Returns any allocation errors that may occur
fn rotateArray(M: []const []const u8, allocator: std.mem.Allocator) ![][]u8 {
// we get the dimensions of the square matrix input
// no check for now if the input is not a square matrix
const numRows = M.len;
std.debug.print("nb rows: {}\n", .{numRows});

// we instantiate the matrix by allocating memory for the rows
const matSlice = try allocator.alloc([]u8, numRows);

// we instantiate each row of the matrix by allocating memory for each column
// we want to modify the slice itself, not its contents, so we use a pointer
for (matSlice) |*row| {
row.* = try allocator.alloc(u8, numRows);
// if we want to initialize with a default value, e.g., 0
// @memset(row.*, 0);
}

for (M, 0..numRows) |row, i| {
var iterSplitedRow = std.mem.splitScalar(u8, row, ' ');
var j: u8 = 0;
while (iterSplitedRow.next()) |splitRow| : (j += 1) {
const value: u8 = try std.fmt.parseInt(u8, splitRow, 10);
// std.debug.print("rowIdx {}, colIdx: {}, v: {}\t", .{ i, j, value });
matSlice[numRows - j - 1][i] = value;
}
}
return matSlice;
}

/// Main function that demonstrates the usage of rotateArray with ArenaAllocator.
/// It creates two matrices, rotates them, and prints the results.
///
/// Note: This function uses an ArenaAllocator, which means all memory
/// allocations are freed at once when the function returns.
pub fn main() !void {
var arena = ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

// stack allocated matrix, compile-time known
const M1 = [_][]const u8{ "1 2 3 4", "5 6 7 8", "9 10 11 12", "13 14 15 16" };
const M2 = [_][]const u8{ "1 2 3", "4 5 6", "7 8 9" };

var result = try rotateArray(&M1, allocator);
std.debug.print("{any}\n", .{result});

result = try rotateArray(&M2, allocator);
std.debug.print("{any}\n", .{result});
}

```


To run this beauty, copy/paste into:

or save it into a file "rotate.zig" and do: `zig run rotate.zig`

or compile it: `zig build-exe rotate.zig` and `./rotate`. Fast as f** 😬

## Elixir code

We firstly produce a list by transforming a string made of space separate numbers into a list with `String.split`,
Then `Enum.map` builds a list of lists.

We return a list of list with 2 nested `Enum.map` and extract a value with 2 `Enum.at`. No instantiation of any kind as we just return from a function. This is nice.

Elixir code

```elixir
defmodule Matrix do
def rotate_matrix(m) do
# Split each row into lists of integers
matrix = Enum.map(m, &String.split(&1))

# Get dimensions
dim = length(matrix)

# Populate the rotated matrix
rotated =
Enum.map(0..(dim - 1), fn row ->
Enum.map(0..(dim - 1), fn col ->
Enum.at(Enum.at(matrix, col), dim - row - 1)
end)
end)

# Convert each row back into a string
Enum.map(rotated, &Enum.join(&1, " "))
end
end

# Example usage:
Matrix.rotate_matrix(["1 2 3", "4 5 6", "7 8 9"]) |> IO.inspect()
Matrix.rotate_matrix(["1 2 3 4", "5 6 7 8", "9 10 11 12", "13 14 15 16"]) |> IO.inspect()

```


You know the story: `elixir rotate.exs`

## Javascript (Node) code

We build an array of arrays, by `splitting` the rows and iterating with `map`.

We **need to** instantiate the array of arrays `result` with a `for` loop where we `push`.

With a nested `for` loop, we access an element with the `[]` notation.

Node code

```js
function rotateMatrix(M) {
// Split each string into arrays of numbers
const matrix = M.map((row) => row.split(" "));

// Get the dimension of the square matrix
const dim = matrix.length;

// produce an empty array of arrays [[],[],[],..]
let rotated = [];
for (let i = 0; i < dim; i++) {
rotated.push([]);
}

for (let row = 0; row < dim; row++) {
for (let col = 0; col < dim; col++) {
rotated[row][col] = matrix[col][dim - row - 1];
}
rotated[row] = rotated[row].join(" ");
}
return rotated;
}

// Example usage:
let M = ["1 2 3", "4 5 6", "7 8 9"];
// [ [ '1', '2', '3' ], [ '4', '5', '6' ], [ '7', '8', '9' ] ]
let result = rotateMatrix(M);
console.log(result);

M = ["1 2 3 4", "5 6 7 8", "9 10 11 12", "13 14 15 16"];
result = rotateMatrix(M);
console.log(result);
```

You know the story: `bun rotate.js`

or paste and run it in the browser's console!

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.