##### August 15th, 2025, ca. 22:00 -0400 ## Today's Topics: Array `@zip` in C3 > [!info]- Article PGP Signature > > To verify this signature, use the public key on the [[Hello, World!|Home Page]] and follow the instructions linked there. > > ``` > -----BEGIN PGP SIGNATURE----- > > iHUEABYKAB0WIQT2vFo+jLf+FWIQ2T8xrKfG/dldbgUCaJ/2rgAKCRAxrKfG/dld > bn0kAQDU+0r5iy0Tz9A77EJQl2G0tmGXBdTBtc4ZbQ4SFM31NQEA4jg0vXOylKuB > lYiXhN6gjjEwQNszcNrWNknxeDtTOQY= > =kVRt > -----END PGP SIGNATURE----- > ``` > ### Today's Contributions - Continued PR in progress: [c3lang#2370](https://github.com/c3lang/c3c/pull/2370) ### Array Zipping For today's discussion, I want to write about a change I have pending in the C3 standard library, which is adding some more native functionality to the core arrays library. #### What do you mean 'zip'? Obviously we all know what a zipper mechanism is and what it does, but to further illustrate the point of this log entry, I want to drop this in here: ![[zipper.gif]] Imagine the purple side on the left is the `left` array of elements, and the red side is the `right` array of elements. The two elements, when zipped, still retain their individual colors, but they are bound together into a single span. When you `@zip` the data in C3, you are either creating a resulting array that contains either `Pair { $typeof(left), $typeof(right) }` or `$typeof(fn $ResultType ($typeof(left), $typeof(right)))` elements, depending on which "flavor" you choose to use. #### How's that work in-code? There are many reasons you'd want to zip data, but most of the time it's either... 1. The two elements of the resulting `Pair` are completely *unrelated*, and only need to be correlated as one unit for a *brief function call* or other process, or 2. You want to [apply a function](https://en.wikipedia.org/wiki/Apply) with two parameters, sourcing each parameter from two disparate lists at the same indices. In non-nerd speak, this means I want to (1) package some data fields together or (2) do some kind of calculation combining two lists. #### Enough talking, code time! Here is the function signature and **brief** explanation of each new macro (as of writing; it's been changing a tad). To see the full explanations and reasoning, you should read the [pull request](https://github.com/c3lang/c3c/pull/2370). > [!faq] Regular `@zip` > > ```swift > <* > ... > > @param [&inout] allocator : "The allocator to use; default is the heap allocator." > @param [in] left : "The left-side array. These items will be placed as the First in each Pair" > @param [in] right : "The right-side array. These items will be placed as the Second in each Pair" > @param #operation : "The function to apply. Must have a signature of `$typeof(a) (a, b)`, where the type of 'a' and 'b' is the element type of left/right respectively." > @param fill_with : "The value used to fill or pad the shorter iterable to the length of the longer one while zipping." > ... > *> > macro @zip(Allocator allocator, left, right, #operation = EMPTY_MACRO_SLOT, fill_with = EMPTY_MACRO_SLOT) @nodiscard > ``` > This function looks intimidating, but it's use is not that complicated. Provide two arrays, `left` and `right`, to zip. If that's all you gave, you'll get back a list of tuples containing elements from each position of each array. Easy. > > If `fill_with` is not explicitly provided, then the result is the length of the ***shortest*** array; otherwise, the shorter array is _filled_ with `fill_with` up until the length of the ***longer*** array. > > Finally, if `#operation` is a valid and conforming function pointer, the function is applied to each input from each array (after padding from `fill_with`, if given). The result is a single array, with a single type (not tuples) derived from the applied function. > [!faq] No-allocator `@zip_into` > > ```swift > <* > ... > @param [inout] left : "Slice to store results of applied functor/operation." > @param [in] right : "Slice to apply in the functor/operation." > @param #operation : "The function to apply. Must have a signature of `$typeof(a) (a, b)`, where the type of 'a' and 'b' is the element type of left/right respectively." > ... > *> > macro @zip_into(left, right, #operation) > ``` > As succinctly as possible: do the same as `@zip`, but store the results of the `#operation` into the `left` slice/array. This is convenient because it doesn't require any memory allocations; however, it can be lacking because (1) it overwrites original data from `left`, and (2) it is limited to the length of `left` _only_. > > By the way, the above means that `#operation` **must** return the same type as `$typeof(left)`. #### Have an example? I sure do. Here's a simple use of each type, with all parameters being utilized from each signature, so you can grok the full utility and magnificence of `zip`. > [!example]- `@zip` with a `fill_with` and a function to apply > > Zip the elements of `right` into `left` via the function `fn TestStructZip (a, b) => a * b`, where `@operator(*)` _is defined_ as `{ l.a * r.a, l.b * r.b }` for the `TestStructZip` type. > > So, for the first iteration, `a = {1, 2}` and `b = {-1, -1}`. The output from the lambda is `{-1, -2}`. > > The next iteration is run because, even though `right` doesn't have a value, `fill_with` supplies a *default input* for `b` of `{2, 3}`. > > Thus, the next iteration is `a = {300, 400}` and `b = {2, 3}`, which yields and stores `{600, 1200}`. That's where the final value comes from. This defaulting of `b` would keep applying for all remaining elements of `left`, if there were more to match with and consume. > > ```cpp > fn void zip_with_fill_with_struct() => @pool() > { > TestStructZip[] left = { {1, 2}, {300, 400} }; > TestStructZip[] right = { {-1, -1} }; > > TestStructZip[] expected = { {-1, -2}, {600, 1200} }; > > TestStructZip[] zipped = array::@tzip(left, right, fn TestStructZip (TestStructZip a, TestStructZip b) => a * b, (TestStructZip){2, 3}); > > test::eq(zipped.len, 2); > foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); > } > ``` > [!example]- `@zip` returning `Pair` values > > This is the same as the above example, except no function is applied to each parameter, and no `fill_with` value is supplied. > > Thus, the result is a simple pairing of each array's elements, up to the length of the ***shorter*** input. > > ```cpp > fn void zip() => @pool() > { > char[] left = "abcde"; > long[] right = { -1, 0x8000, 0 }; > > Pair{char, long}[] expected = { {'a', -1}, {'b', 0x8000}, {'c', 0} }; > > Pair{char, long}[] zipped = array::@tzip(left, right); > > test::eq(zipped.len, 3); > foreach (i, c : zipped) assert(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); > } > ``` > [!example]- `@zip_into`, simple as > > Zip the elements of `right` into `left` via the function `fn (a, b) => a + (char)b.len`. > > So, for the first iteration, `a = 1` and `b = "one"`. Running that through the lambda gives: `1 + "one".len`, which equals `4` since the length of the string "one" is `3`. > > This is repeated up until the length of the ***shorter*** input array, because a `fill_with` value was _not_ provided, hence the length of the `expected` slice. > > ```cpp > fn void zip_into() > { > char[] left = { '1', '2', '3', '4' }; > String[6] right = { "one", "two", "three", "four", "five", "six" }; > > char[] expected = { '4', '5', '8', '8' }; > > array::@zip_into(left, right, fn (a, b) => a + (char)b.len); > > test::eq(left.len, 4); > foreach (i, c : left) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); > } > ``` ### Summary Wow, what a week! And what a great time I had being able to collect my thoughts at the end of each day. I really hope to continue this journey, especially when I get into what it means to build my OS from scratch. ==Until Monday==, bye!