Strange Range Syntax

Reading Can i have a range as function parameter? I had an idea how you could use (abuse) slice syntax to create a kind-of range object:

const Example = struct {
    many: [20]f32,

    pub const zeroes: Example = .{ .many = @splat(0) };

    pub fn subslice(self: *Example, item_range: anytype) []f32 {
        return range.apply(&self.many, item_range);
    }
};

pub fn main() !void {
    var example: Example = .zeroes;

    example.subslice(range.over[0..1])[0] = 99.0;

    const subslice = example.subslice(range.over[5..]);
    subslice[0] = 123.0;
    subslice[1] = 0.5;
    subslice[2] = 3.9;
    std.debug.print("{}\n", .{example});
}

pub const range = struct {
    const RangeElement = struct { do_not_access_or_deref: u8 };
    pub const Closed = []allowzero const RangeElement;
    pub const OpenEnded = [*]allowzero const RangeElement;
    pub const over: OpenEnded = @ptrFromInt(0);

    pub fn hasLength(comptime Sliceable: type) bool {
        return comptime info: switch (@typeInfo(Sliceable)) {
            .array => true,
            .pointer => |p| switch (p.size) {
                .c, .many => false,
                .slice => true,
                .one => continue :info @typeInfo(p.child),
            },
            else => false,
        };
    }
    pub fn Result(Sliceable: type, Range: type) type {
        return if (hasLength(Sliceable) or hasLength(Range))
            []std.meta.Elem(Sliceable)
        else
            [*]std.meta.Elem(Sliceable);
    }

    pub fn applyClosed(sliceable: anytype, range_: Closed) []std.meta.Elem(@TypeOf(sliceable)) {
        return sliceable[@intFromPtr(range_.ptr)..][0..range_.len];
    }
    pub fn applyOpenEnded(sliceable: anytype, range_: OpenEnded) Result(@TypeOf(sliceable), @TypeOf(range_)) {
        return sliceable[@intFromPtr(range_)..];
    }

    pub fn apply(
        sliceable: anytype,
        /// Needs to be of type `Closed` or `OpenEnded`
        range_: anytype,
    ) Result(@TypeOf(sliceable), @TypeOf(range_)) {
        const Sliceable = @TypeOf(sliceable);
        const Range = @TypeOf(range_);
        return if (hasLength(Sliceable) or hasLength(Range))
            applyClosed(sliceable, if (comptime hasLength(Range)) range_ else range_[0 .. sliceable.len - @intFromPtr(range_)])
        else
            applyOpenEnded(sliceable, range_);
    }
};

const std = @import("std");

I am not quite sure whether I like it or not.
What do you think?

1 Like

Hey, that’s pretty clever, kudos for that.

That said – Zig’s biggest selling point (for me) is the simplicity and the fact that it doesn’t really let you extend the syntax. Have you ever tried to read extended Nim code you didn’t write? It’s incredibly opaque and stylized – Really Cool, but style points don’t get us working programs (unfortunately).

It’s ‘fun’ getting to see people be clever, but it usually end up being a distraction from the actual program (debugging your language knowledge vs debugging the program).

I’d personally rather just have a simpler function like: range(startUsize, endUsize) that returns an array literal, but probably even better is if I just… declared the array literal.

Maybe I’m just too rigid, maybe I’m not working on anything where the short-hand would improve readability and I’m too unimaginative to think of a good example, but if I saw this used extensively throughout a codebase, I’d consider it a detraction from overall code quality.

Not enough of a detraction to actually complain about it (probably), but I would probably grumble to myself a little.

2 Likes

It definitely feels more like a candidate for What's the most cursed Zig you can write?, than a realistic/useful way to use the language, especially because if you for example try and print the range.over[a..b] or use it with anything else that dereferences it, you will likely segfault your program (or read/print garbage).

Still maybe there are obscure cases where it makes sense (that I haven’t thought of), for most purposes I think something like this would be way better (sketch):

const Range = packed struct {
    start:u32,
    len:?u31,

    pub fn closed(start:u32, len:u31) Range {
        return .{ .start = start, .len = len };
    }
    pub fn openEnded(start:u32) Range {
        return .{ .start = start, .len = null };
    }
    pub fn format(self:Range, writer:*std.Io.Writer) !void {
        ...
    }
};
var range:Range = .openEnded(7);

Depending on use case you could even add a step size, possibly even with negative values, for reverse iteration, allowing optional start or end, inclusive or exclusive start/end.

2 Likes

:man_shrugging:

I think it’s really cool that you can technically do that even if I think I’d rather nobody actually did. It’s all context specific though, right?

My biggest Zig-Sin is smuggling parameter declarations into inline functions. Can’t accidentally dereference something that doesn’t have a syntactical way to reference it after you initialize it!

People keep telling it will probably break, but I keep telling them ‘that’s technically true for Zig as a whole’. Is it bad practice regardless? Yep, probably! Am I going to stop doing it before Andrew himself tells me it’s going to break? No.

3 Likes