dwyl / dwyl/learn-zig

Ziggy zip with `for` loops

Open
#7 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

## Quick review of comprehensions, "for-like" in Elixir

### Cross-product

In Elixir, a double "for" loop via the so-called "comprehensions" goes like this:

```elixir
u = 1..5
v = 1..3
for i<- u, j<- v, do: {i,j}

# equivalently:
Enum.map(u, fn i -> Enum.map(v, fn j -> {i,j} end) end)
```

This is in fact a **cross product** of `[1,2,3, 4, 5]` with `[1,2,3]`: it gives you 5x3 tuples.

```
[
{1, 1},{1, 2},{1, 3},
{2, 1},{2, 2},{2, 3},
{3, 1},{3, 2},{3, 3},
{4, 1},{4, 2},{4, 3},
{5, 1},{5, 2},{4, 3}
]
```

### Zip

If you want a **zip** product, well `Enum.zip`! (the arrays must have the same length).

```elixir
u = 1..3;
v = 2..4;
Enum.zip(u,v)
```

gives you 3 tuples as expected.
```
[{1, 2}, {2, 3}, {3, 4}]
```

If you want to zip more vectors, you need more work.
Suppose we have 3 arrays of length 4:

```
u = [1,2,3,4]
v = [2,3,4,5]
w = [3,4,5,6]
```

then in Elixir:

```elixir
for j<- 0..3 do
Enum.reduce([u,v,w], [], fn row, acc -> acc ++ [Enum.at(row, j)] end)
end
```

gives you the expected 4 lists of length 3:

```
[
[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]
]
```

## Zig

### Cross product

If we want a "cross" product of two vectors, then it is a simple double embedded `for` loop.
We used the struct `ArrayList` because we can add simply the element `i,j ` during the iteration.

```zig
fn cross(a: []const i8, b: []const i8, allocator: std.mem.Allocator) ![][2]i8 {
var res = std.ArrayList([2]i8).init(allocator);

for (a) |ai| {
for (b) |bj| {
try res.append([2]i8{ ai, bj });
}
}
return res.toOwnedSlice();
}
```

In the signature, we have two inputs as **a slice of an array of `u8`s**.
An array is build as: `const u = [_]i8{ 1, 2, 3, 4 };`.
Its type is `@TypeOf(&u)` which is `*const [4]i8`.
A slice of `u` can be build with `const u_slice = u[0..]` (for the full length, has the same type as `&u `.

Since slices are pointers, we pass pointers (`&u`) when we call the function with:

```zig
const u = [_]i8{ 1, 2, 3, 4 };
const v = [_]i8{ 5, 6, 7, 8 };

try cross(&u, &v, allocator);
```

We obtain the expected cross product with `4x4` tuples.

```
{
{ 1, 5 }, { 1, 6 }, { 1, 7 }, { 1, 8 },
{ 2, 5 }, { 2, 6 }, { 2, 7 }, { 2, 8 },
{ 3, 5 }, { 3, 6 }, { 3, 7 }, { 3, 8 },
{ 4, 5 }, { 4, 6 }, { 4, 7 }, { 4, 8 }
}
```

The type `[]const i8` is a slice of an array of `i8`s, this explains the signature of the function.

Since we know that we have 2 entries, u and v, don't know the length of an array in advance, we need to build a dynamic array.
We use `ArrayList` because for each `ai`, we will iterate again to append the `bj`s, so that dynamic array makes it easy.
To free the memory used, we call `toOwnedSlice()` which frees the memory and returns just the data without the overhead of the struct.

### First "easy" Zip: exactly 3 vectors of any length

In Zig, if you know in advance the number of vectors you want to zip, you can use a simple `for` with exactly the number of entries.
When you zip together `3` vectors of size `n`, you get `n` vectors of size `3`.
An example to explain. I have 3 vectors to zip. I hard code directly the number of entries, given by the number of arguments of my function: three slices `a`, `b` and `c` which are arrays of a given (and identical) fixed size.

> you notice I am allocating memory for the type `[3]i8`, and returning **a slice of slices of length 3**.

```zig
const std = @import("std");

fn zip(a: []const i8, b: []const i8, c: []const i8, allocator: std.mem.Allocator) ![][3]i8 {
if (a.len != b.len or a.len! != c.len) return error.OutOfRange;

// we know the size in advance so we can allocate it.
var res = try allocator.alloc([3]i8, a.len);

for (a, b, c, 0..) |ai, bi, ci, i| {
res[i] = [3]i8{ ai, bi, ci };
}
return res;
}
```

We test this with 3 arrays of length 4:

```zig
test "zip" {
// let Zig compute the length with "_".
const u = [_]i8{ 1, 2, 3, 4 };
const v = [_]i8{ 5, 6, 7, 8 };
const w = [_]i8{ 9, 10, 11, 12 };

const res = try zip(&u, &v, std.testing.allocator);
defer allocator.free(res);

for (res2) |pair| {
std.debug.print("({d}, {d}), ", .{ pair[0], pair[1] });
}
std.debug.print("\n", .{});
}
```

gives 4 arrays of 3 elements as expected.

```
{1, 5, 9 },
{2, 6, 10},
{3, 7, 11},
{4, 8, 12}

```

### Variadic version: any number of vectors of any length

Lets say we have arrays (all of the given length `n`), but of unknown quantity that we want to zip. 🤯

Our input will be a slice of say `n` arrays of length `m`: we will produce a kinda matrix of `m` rows with `n` columns (a `m x n` matrix).

Firstly the signature of our function. The input is a slice of slices, so `[]const []const i8`. A pointer of pointers.

Then we have arrays in the form say `[_]i8{ 1, 2, 3, 4 }`.
We need to build a slice of these.

:exclamation: We can't do it directly in the function call but need to do it ahead:

We build an array of slices (Zig asks to pass references of the inner arrays) because we used `[_]` for Zig to infer the length.

```zig
const input = [_][]const i8{ &u, &v, &w };
```

Then pass a reference to our function pompously called "variadicZip".

```zig
variadicZip(&input, allocator)
```

:exclamation: After lots of researching, it seems that another way is to instantiate via `&.{...}`:

```zig
variadicZip(&.{ &u, &v, &w}, allocator)
```

The function allocates memory to build dynamically a slice (named `result` here) of arrays with a calculated length (the length of the internal arrays).

:exclamation: you can use `ArrayList.initCapacitor` because you know that you will need to allocate subarrays of length `len`.

In the second internal loop, we simply allocate memory for the rows we will append to the `result`. This row is simply build by assigning a value at the given index by the iteration.

:exclamation: After lots of errors, it seems that you need to use `appendAssumeCapacity` to append this row to your result ArrayList (as `append` throws an error. I understood no bound checking...).

```zig
fn variadicZip(arrays: []const []const i8, allocator: std.mem.Allocator) ![][]i8 {
if (arrays.len == 0) return error.EmptyInput;

const nb_arrays = arrays.len;
const len = arrays[0].len;

for (arrays[1..]) |arr| {
if (arr.len != len) return error.UnequalLengths;
}

var result = try std.ArrayList([]i8).initCapacity(allocator, len);

for (0..len) |i| {
var row = try allocator.alloc(i8, nb_arrays);

for (arrays, 0..) |array, j| {
row[j] = array[i];
}
result.appendAssumeCapacity(row);
}
return result.toOwnedSlice();
}
```

We test this with 3 arrays of length 4:

```zig

test "zip" {
const u = [_]i8{ 1, 2, 3, 4 };
const v = [_]i8{ 5, 6, 7, 8 };
const w = [_]i8{ 9, 10, 11, 12 };

const input = [_][]const i8{ &u, &v, &w };
const res = try variadicZip(&input, std.testing.allocator);

// as noted, we can also use a tuple:
// const res = try variadicZip(&.{&u, &v, &w}, std.testing.allocator)

for (res) |arr| {
std.debug.print("{any}, ", .{arr});
}
```

and we get 4 arrays of length 3:

```
{ 1, 5, 9 },
{ 2, 6, 10 },
{ 3, 7, 11 },
{ 4, 8, 12 }
```

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.