Pythonlike hasattr and getattr functions for Zig

I was looking to zig as a new language to experiment and see what benefits it brings. So before that I had experience in Python, Golang, Elixir and Zig was something different and more low level… probably no…
Work with allocators and all that process of managing memory is different from what I saw before, it is true. And of course it is pretty small and fast. I have some hobbies relate to play with microcontrollers and self-hosting multiple different things so memory usage and speed are supper important there.

I was thinking about something to experiment and make language easier to use. And I started with something I used in Python to make things that were unexpected for other languages. I like when I can access some props or functions by simply asking it by name like string.

So, I added small lib zattr with two public functions hasattr and getattr. I do not know if language expects such in main lib but I think it would be useful.

I tried to add setattr but it was a bit complicated, because it is not dynamic language and I was able to set only existing properties. Not sure if it is possible to implement function setting too… Okay a lot of text. Here is the implementation I did kaimanhub/zattr - Codeberg.org.

std.meta namespace which contains such utilities is expected to be remove from stdlib by moving useful functions to buildins or removing unnecessary once. So I wouldn’t expect it to be accepted to stdlib.

This is intentionally not possible to implement. This would be possible in c++ reflections for example.

Most of the time you don’t want generic hasattr since you can’t do anything with it. You’ll need to get more detailed information about type of attribute and it will require reimplementing this function

2 Likes

There is already built in:

@hasField(comptime Container: type, comptime name: []const u8) bool

Returns whether the field name of a struct, union, or enum exists.
The result is a compile time constant.
It does not include functions, variables, or constants.

and

@hasDecl(comptime Container: type, comptime name: []const u8) bool

Returns whether or not a container has a declaration matching `name`.
1 Like

Yes, I used them in my implementation.These built ins are pretty good.
Idea of hasattr and getattr only to wrap them and use one tool to check both fields and declarations. It is not something that big. Implementation is super small.

I have not done setattr unfortunately. Any thought what built ins cold be used in that case?

1 Like

Yes, I used them in my implementation.These built ins are pretty good.
Idea of hasattr and getattr only to wrap them and use one tool to check both fields and declarations. It is not something that big. Implementation is super small.

Note that Decl’s and Fields are different. Fields are like instance attributes, where Decls are kinda like class attributes, if we want to extend the Python analogy.

For example:

class TestStruct:
    
    def __init__(self, name: str, age: int, score: float):
        self.name = name
        self.age  = age
        self.score = score

    def greet(self: TestStruct):
        pass

    def add_numbers(a: int, b: int):
        pass

    def get_age(self: TestStruct) -> int:
        pass

is roughly equivalent to your:

const TestStruct = struct {
    name: []const u8,
    age: u32,
    score: f32,

    fn greet(self: TestStruct) void {
        std.debug.print("Hello, I'm {s}\n", .{self.name});
    }

    fn add_numbers(a: i32, b: i32) i32 {
        return a + b;
    }

    fn get_age(self: TestStruct) u32 {
        return self.age;
    }
};

@hasDecl will only report on the static members of the container (functions, container level constants and variables) where @hasField will give the the “instance fields”, ie the parts that actually take up the bytes of a Struct (in this case name, age, score). In python there isn’t much of a differentiation made, but i’m not sure when I would want to ducktype-check if a struct had a field -or- a method named “speak”.

I have not done setattr unfortunately. Any thought what built ins cold be used in that case?

This is intentionally not possible to implement. This would be possible in c++ reflections for example.

This is only partially true. You can do a set attribute on fields using the field builtin

@field(lhs: anytype, comptime field_name: []const u8) (field)

this can be used in the LHS of an assignment:

@field(obj, "name") = "JimboJones";

But this is only possible with fields, not declarations. I.e you can’t add methods to an struct at comptime or runtime.

6 Likes

One caveat here: if you have a field that is a function pointer, you could swap out the underlying function at run time. But you could not add a new method. Nor could you change the function signature

const Editable = struct {
    const Param = union(enum) {
        a: u32,
        b: f64,
    };
    behavior: *const fn (self: *Editable, param: Param) Param,
};

fn behaviorA(self: *Editable, param: Editable.Param) Editable.Param {
    _ = self;
    return .{
        .a = param.a + 1,
    };
}

fn behaviorB(self: *Editable, param: Editable.Param) Editable.Param {
    _ = self;

    return .{
        .b = param.b + 1.0,
    };
}

pub fn main() void {
    var e = Editable { .behavior = &behaviorA };

    // Sometime later
    e.behavior = &behaviorB;

   // Or using OPs potential `setattr`
    setattr(e, "behavior", &behaviorB);
}
3 Likes

This isn’t true. From the @field docs:

Performs field access by a compile-time string. Works on both fields and declarations.

const std = @import("std");

const Point = struct {
    x: u32,
    y: u32,

    pub var z: u32 = 1;
};

test "field access by string" {
    const expect = std.testing.expect;
    var p = Point{ .x = 0, .y = 0 };

    @field(p, "x") = 4;
    @field(p, "y") = @field(p, "x") + 1;

    try expect(@field(p, "x") == 4);
    try expect(@field(p, "y") == 5);
}

test "decl access by string" {
    const expect = std.testing.expect;

    try expect(@field(Point, "z") == 1);

    @field(Point, "z") = 2;
    try expect(@field(Point, "z") == 2);
}

This part is true, but I’m not sure how it’s relevant, since you can’t add fields using @field either.

4 Likes

Took me a while to realize it was global variable access and not comptime mutability)

2 Likes

I stand corrected. I figured @field would only work for fields. It is kinda annoying because in most other cases fields and decls behave differently.

This part is true, but I’m not sure how it’s relevant, since you can’t add fields using @field either.

With python background of the library I mentioned it, as in python you can use setattr to create new attributes on an object, not just set existing ones. But it is a valid point, you can’t add new fields to a struct as well.

5 Likes

It was surprising to me too :upside_down_face:

5 Likes

Tried to add basic setattr with a limited functions. Have not updated doc and tests yet.
Link to commit on codeberg > Making sure you're not a bot!

it works better than I expected. Not great of course.
Found some magic in zig… did not know that we could use functions as field. It offers some flexibility. zig is nice.

However, yes we can’t add new fields… at least I have not found a way to do that without building a new object or something like that.

Thanks for suggestions. Updated docs and test a bit. So example of what lib does with setattr :

const std = @import("std");
const zattr = @import("zattr");

const TestStruct = struct {
    name: []const u8,
    age: u32,
    greet_fn: *const fn (self: TestStruct) void,

    pub fn greet(self: TestStruct) void {
        std.debug.print("Hello, I'm {s}\n", .{self.name});
    }

    pub fn add_numbers(a: i32, b: i32) i32 {
        return a + b;
    }
};

fn my_test_greet_fn(self: TestStruct) void {
    std.debug.print("Hello {s}! \n", .{self.name});
}

pub fn main() !void {
    var obj = TestStruct{ .name = "O", .age = 24, .greet_fn = undefined };

    std.debug.print("hasattr examples:\n\n", .{});
    std.debug.print("Has 'name': {}\n", .{zattr.hasattr(obj, "name")});
    std.debug.print("Has 'test': {}\n", .{zattr.hasattr(obj, "test")});

    // setattr examples
    zattr.setattr(&obj, "age", 66 - obj.age);
    zattr.setattr(&obj, "name", "N");
    zattr.setattr(&obj, "greet_fn", &my_test_greet_fn);

    std.debug.print("\ngetattr examples:\n\n", .{});
    const age = zattr.getattr(obj, "age");
    std.debug.print("Age: {d}\n", .{ age });
    zattr.getattr(obj, "greet_fn")(obj);
    
    const greet = zattr.getattr(obj, "greet");
    greet(obj);

    if (zattr.hasattr(obj, "add_numbers")) {
        const res = zattr.getattr(obj, "add_numbers")(31, 11);
        std.debug.print("Result is: {d}", .{res});
    }
}```