Are there support for iterator adaptors and consumers in the standard library?

I am working through Zig exercise on exercism and I just worked on an exercise whose solution was:

const std = @import("std");
pub fn score(s: []const u8) u32 {
    var final_score: u32 = 0;
    for (s) |ss| {
        final_score += point(std.ascii.toUpper(ss));
    }
    return final_score;
}

fn point(letter: u8) u32 {
   return switch(letter) {
      'A', 'E', 'I', 'O', 'U', 'L', 'N', 'R', 'S', 'T' => 1,
      'D', 'G' => 2,
      'B', 'C', 'M', 'P' => 3,
      'F', 'H', 'V', 'W', 'Y' => 4,
      'K' => 5,
      'J', 'X' => 8,
      'Q', 'Z' => 10,
      else => unreachable
    };
}

Instead of manually iterating, in Rust I could have solved it like this

pub fn score(s: &str) -> u32 {
    s.to_ascii_uppercase()
        .chars()
        .map(|letter| point(letter))
        .sum()

    // s.to_ascii_uppercase()
    //     .chars()
    //     .map(|letter| point(letter))
    //     .reduce(|acc, letter| acc + letter)
    //     .unwrap()
}

fn point(letter: char) -> u32 {
    match letter {
        'A' | 'E' | 'I' | 'O' | 'U' | 'L' | 'N' | 'R' | 'S' | 'T' => 1,
        'D' | 'G' => 2,
        'B' | 'C' | 'M' | 'P' => 3,
        'F' | 'H' | 'V' | 'W' | 'Y' => 4,
        'K' => 5,
        'J' | 'X' => 8,
        'Q' | 'Z' => 10,
        _ => panic!("unreachable"),
    }
}

Which leads me to ask the following questions,

  • In Zig’s standard library, is there support for iterators
  • Is there support for iterator adaptors like map, filter etc
  • Is there support for iterator consumers like reduce, fold, sum etc

I tried looking in the standard library but I could not find.

If the answer is “no there isn’t these kind of stuff in Zif”, my follow up question would be what is the idiomatic way in Zig to approach these kind of problems that iterator adaptors and consumers would be used for? Do it manually as I have done above?

See the Fluent library.
Showcase in ziggit: Fluent: Algorithmic Chaining Library for Slice Manipulation

3 Likes

Zig is an imperative language, and as such there is no map/reduce/fold and the like in the language or the standard library. Your Zig implementation is perfectly idiomatic. There are iterators in the standard library but they are used in an imperative way. The conventional way to implement an iterator is as a struct with a next method on it that returns an optional. This can be used in a while loop to consume elements from the iterator until the next method returns null.

var lines_iter = std.mem.tokenizeScalar(u8, file_contents, '\n');
while (lines_iter.next()) |line| {
    // Do something with `line`
}
1 Like

Well, so also is Rust :slight_smile:

Actually, I was just thinking of this. It would be nice to have an @map, @reduce, @filter, etc builtin functions for slices. they would go equally well in std.mem, but they’re such fundamental programming concepts, and in zig so are slices that it feels like a natural pairing. I don’t think zig being imperative prohibits this. These are well defined actions on a set of data, so their outcome is quite imperative. They’re just extra juicy tools to have in the toolkit, and it seems lots of people think so as well based on how often I see them reimplemented in personal libraries (my own included).

edit: just realized there is already an @reduce (for simd vectors), which just goes to show that these types of functions are a natural fit for zig.

1 Like

How would @filter work without allocation?

1 Like

actually, this is a great point that I think shows those functions would best fit as std.mem functions. As there are already functions there that require an allocator such as std.mem.join.
or maybe, @filter just requires a buffer of equal size pointed to at a minimum, so could also use allocated memory, but otherwise is more like std.fmt.bufPrint? (and then returns the count of entries copied to the buffer?)

To be exact, either an optional or an error union (where the non-error value is an optional), like for example std.zip.Iterator.

Not really.

It’s more something in between an imperative and a functional language. After all the two strongest influences in the language are C++ and OCaml.

Based on comments I’ve seen by the Zig team, or at least Andrew, I would be shocked to see this added to the std library. They prefer explicit use of for, while, if, switch, etc. I suggest trying to do without iterator adapters and combinators, since that is how Zig is intended to be used. For me it was an adjustment after using them so much in Rust, but I found I really didn’t need them.

PS. I would not call Rust a functional language, but Rust iterators do come from functional programming. Some imperative languages adopt certain things from functional programming and some don’t, by choice of the designer. I can’t think of any off-hand that Zig adopted, although tagged unions might be considered one.

PSS. Also note that in spite of the Zig while (...) |...| feature being usable as an iterator, it is more general than that. Any expression that returns an optional or error union works. So I don’t think of Zig as having iterators per se.

1 Like

@reduce is an intrinsic, ‘modern’ processors in the loose sense will have CPU instructions which directly correspond to what it does. It’s only ‘functional’ in the sense that fundamental arithmetic operations may be viewed (correctly!) as functions.

It doesn’t exist because reducing an array with a function is elegant functional style. It exists because having the compiler recognize an opportunity to apply the intrinsic is complex and brittle, and it’s therefore better to direct the compiler to do so with a builtin.

Welcome to Ziggit btw :slight_smile:

3 Likes

Yes, tagged union (together with switch) and optionals come from functional programming. And well, functional programming pretty much is programming theory (at least the parts which I learned at university) put into something you can use for actually writing programs.

1 Like

Small nitpick first: It’s PPS, not PSS. PS stands for “post scriptum” which is Latin for “after writing”, so PPS stands for “after after writing”. Anyway, now to what you wrote.

As Bjarne Stroustrup once said, standard library design is language design. These days I would even says that the tooling design around the language is part of language design, too. All of these influence the actual developer experience (and productivity), not just the design of the language itself (although I do think that we need a new name to better differentiate the language itself and all of these things put together).

With that in mind, I would say that Zig has iterators, just like it has async IO. It’s just part of the standard library.

Wow, I must have missed it. Where is general support for iterators in std? Oh, I do see std.mem.reverseIterator. But no other general support that I can find.

Search for next in the search bar.

You’re right that there are lots of iterator impls. Just very little general support for iterators.

Having iterators and using them, isn’t necessarily the same thing as encouraging functional programming patterns that also make use of iterators. Zig does the former and from what I have seen discourages the latter.

1 Like

What all do you need?

const some_val = find: while(iter.next()) |item| {
     if (match(item)) break :find item;
} else default_value;

Seems pretty well supported to me!

2 Likes

So, the direct answer to the question is “no (but wait, there’s more)”.

In Zig, iterators (like interfaces) are a user-space design pattern. Where appropriate, the pattern is used in various parts of the std. There is no general/generic language feature for iterators, which would imply traits or similar language features which don’t exist in Zig.

4 Likes

:slight_smile:

It’s amazing to me how well Zig’s basic operators work so well together, and how I almost never need anything else. I could have sworn I couldn’t live without capturing closures any longer, but as it turns out, I can.

(And you don’t even need the labeled break above.)

2 Likes