Comparison Zig, Elixir, Node on a simple matrix rotation algorithm
- 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.