Can binary methods work with equal sign

Premise: This is a very unimportant question and I’m not proposing any feature request. Just a bit of brainstorming,

Let’s say I have a custom number / struct and I want to do a binary operation on it.

const Foo = struct {
    data: f32,

    fn add (a: Foo, b: Foo) Foo {
        return .{ .data = a.data + b.data};
    }
};

If I want to accumulate on a variable I can use the add method and assign back, but this requires referencing the variable twice.

var a: Foo = .{ .data = 5};
const b: Foo = .{ .data = -3};

a = .add(a, b);

If a is not a local variable but requires array or field access it becomes slightly more cumbersome and error prone.

my_struct.array[index + stride*i] = .add(my_struct.array[index + stride*i], b);

An alternative is to provide a variation of add that does that in place, taking a pointer


const Foo = struct {
    data: f32,

    fn addInplace (a: *Foo, b: Foo) void {
        const temp = a;
        a = .{ .data = temp.data + b.data};
    }
};

a.addInplace(b);

Can there be a syntax for doing the latter by using the add method?
Something like?

a .= .add(b);

There is really no benefit from adding such a thing beyond not typing a couple more characters, that is not something zig cares about, the only reason decl literals exist at all is that typing out generated types can be very verbose and you generally already know the type so there is no benefit in seeing it again, that is a lot more saving than a couple characters.

a = .add(a, b) or a = a.add(b) is not hard at all.

* Favor reading code over writing code.
* Only one obvious way to do things.

1 Like
pub fn main() !void {
    var array: [3]Foo = @splat(.zero);
    std.debug.print("array: {any}\n", .{array});
    update(&array[1], .add, .{.init(7)});
    std.debug.print("array: {any}\n", .{array});
}

const Foo = struct {
    data: f32,

    pub const zero: Foo = .{ .data = 0 };

    pub fn init(data: f32) Foo {
        return .{ .data = data };
    }

    pub fn add(a: Foo, b: Foo) Foo {
        return .{ .data = a.data + b.data };
    }
};

pub fn update(element: anytype, comptime op: FindOps(@TypeOf(element)), args: Args(@TypeOf(element), op)) void {
    const E = @typeInfo(@TypeOf(element)).pointer.child;
    const func = @field(E, @tagName(op));
    element.* = @call(.auto, func, .{element.*} ++ args);
}

pub fn FindOps(comptime P: type) type {
    const E = @typeInfo(P).pointer.child;

    const EF = std.builtin.Type.EnumField;
    var fields: []const EF = &.{};
    var value: usize = 0;
    for (std.meta.declarations(E)) |d| {
        const valid_op = switch (@typeInfo(@TypeOf(@field(E, d.name)))) {
            .@"fn" => |f| f.params.len > 0 and f.params[0].type == E,
            else => false,
        };
        if (valid_op) {
            fields = fields ++ [1]EF{.{
                .name = d.name,
                .value = value,
            }};
            value += 1;
        }
    }
    return @Type(.{ .@"enum" = .{
        .tag_type = std.math.IntFittingRange(0, value - 1),
        .fields = fields,
        .decls = &.{},
        .is_exhaustive = true,
    } });
}

pub fn Args(comptime P: type, comptime op: FindOps(P)) type {
    const E = @typeInfo(P).pointer.child;
    const func = @TypeOf(@field(E, @tagName(op)));
    return TupleFromParams(@typeInfo(func).@"fn".params[1..]);
}

pub fn TupleFromParams(params: []const std.builtin.Type.Fn.Param) type {
    var types: [params.len]type = undefined;
    for (params, 0..) |p, i| types[i] = p.type.?;
    return std.meta.Tuple(&types);
}

const std = @import("std");