#decls in reified types workaround

1 messages · Page 1 of 1 (latest)

sweet flax
#

Zig errors when trying to reify a std.builtin.Type of variant Struct that has a non-empty decls field (to be expected, since std.builtin.Type.Declaration holds just a name, and no body). is there a workaround for that? can I somehow programmatically create a type, with fields of my choosing, and declarations with statically known names whose bodies I compute? or am I forced to do some ugly thing such as:

struct {
    fields: MyComputedFields(...), // all field accesses will look like `name.fields.something` (rather then `name.something`), which is ugly.

    const a = myComputedDeclA(...),
    const b = myComputedDeclB(...),
    const c = myComputedDeclC(...),
}

something like Go's struct embedding would be a way to solve this

and more generally: what's the reason reifed types can't have decls? and is the type inspection API subject to change? it currently looks to have many oddities, and some design decisions I feel are just wrong (though those could be because I don't understand some things...)

hybrid salmon
#

can you show the code and how you were trying to reify it?

sweet flax
#

I'm currently just messing around with the type inspection system, so I don't have any built code. let me create something quickly just to show a potential usage I want...

hybrid salmon
#

i just wanna see the code that errored

sweet flax
#

ah, sure

#
const Foo: type = blk: {
    const foo = std.builtin.Type{
        .Struct = std.builtin.Type.Struct{
            .layout = .auto,
            .backing_integer = null,
            .fields = &.{
                std.builtin.Type.StructField{
                    .name = "abc",
                    .type = []const u8,
                    .default_value = null,
                    .is_comptime = false,
                    .alignment = @alignOf([]const u8),
                },
            },
            .decls = &.{
                std.builtin.Type.Declaration{
                    .name = "def",
                },
            },
            .is_tuple = false,
        },
    };
    break :blk @Type(foo);
};
#
error: reified structs must have no decls
    break :blk @Type(foo);
               ^~~~~~~~~~
left sandal
hybrid salmon
sweet flax
#

will write what I have in mind, a moment...

#
fn NumberBlob(comptime field_names: []const [:0]const u8) type {
    comptime {
        var fields: []const std.builtin.Type.StructField = &.{};
        for (field_names) |field_name| {
            fields = &(fields[0..].* ++ .{
                std.builtin.Type.StructField{
                    .name = field_name,
                    .type = i64,
                    .default_value = null,
                    .is_comptime = false,
                    .alignment = @alignOf(i64),
                },
            });
        }

        return @Type(std.builtin.Type{
            .Struct = .{
                .layout = .auto,
                .backing_integer = null,
                .fields = fields,
                .decls = &.{std.builtin.Type.Declaration{
                    // here I hallucinate some capabilites Zig doesn't have
                    .name = "sum",
                    .body = struct {
                        fn sum(
                            self: @This(), // how can we refer to the result of the reification?
                        ) i64 {
                            return sumFunc(field_names, self);
                        }
                    }.sum,
                }},
                .is_tuple = false,
            },
        });
    }
}

fn sumFunc(comptime field_names: []const []const u8, val: anytype) i64 {
    var res: i64 = 0;
    inline for (field_names) |field_name| {
        res += @field(val, field_name);
    }
    return res;
}
#

somewhat bulky, I'll explain what's going on...
NumberBlob is a type-generating function that takes in field names. for each name in the input there'll be a field in the resulting type, of type i64.
also, NumberBlob would generate a sum method, that returns the sum of all of the fields in the type, so that a user can just blob.sum()

tranquil jasper
#

imo that'd be a lot simpler as a disconnected function

fn sum(v: anytype) i64 {
  var res: i64 = 0;
  inline for (@typeInfo(@TypeOf(v)).Struct.fields) |field| {
    res += @field(v, field.name);
  }
  return res;
}

then just sum(blob)

sweet flax
#

that's just an example. I want to be able to create computed methods / constants / variables of arbitrary names in programmatically created types.

#

further, if sum is a method it's much less likely to pass it an incorrect type, making incorrect usage much rarer.

tranquil jasper
#

incorrect usage is a compile error, so imo not horrible user experience. i guess you could do a wrapping type like

pub fn NumberBlob(comptime field_names: []const [:0]const u8) type {
  inner: MakeNumberBlobInner(field_names),

  pub fn sum(self: @This()) i64 {
    var res: i64 = 0;
    inline for (@typeInfo(@TypeOf(self.inner)).Struct.fields) |field| {
      res += @field(self.inner, field.name);
    }
    return res;
  }
}
sweet flax
#

a wrapping type is exactly what I'm trying to avoid.

tranquil jasper
#

is there something a wrapping type prevents you from doing?

sweet flax
#

not sticking a .inner every time I want to access something, making the construction mechanism invisible.

#

currently types created with @Type don't have all of the abilities of types created with struct/enum/union/etc, which I find puzzling.

tranquil jasper
#

i guess a disconnected function would be the less of two evils then. also from what i've been told its just about simplifying the compiler

sweet flax
#

the question is about a solution with no evil, if such even exists.

tranquil jasper
#

maybe something with comptime fields? could be janky for methods tho

sweet flax
tranquil jasper
#

comptime fields are like decls but you access them through a struct instance

#

i would imagine a function comptime field would end up being blob.sum(blob) though

sweet flax
#

what would be their usage? is there an example of them in the wild? (am trying to understand what they allow that cannot be done with decls, putting aside this very question)

tranquil jasper
#

might be somewhere in the docs but heres an example

const Blob = struct {
    x: i64,

    comptime special_number: i64 = 10,
    comptime sum: fn (blob: @This()) i64 = blobSum,
};

fn blobSum(blob: Blob) i64 {
    return blob.special_number * blob.x;
}

pub fn main() void {
    var blob: Blob = .{ .x = 5 };
    blob.x = 10;
    std.log.info("{}", .{blob.sum(blob)});
}
#

they have to have a default value'

sweet flax
tranquil jasper
#

don't think so

#

blob.sum isn't a method, so calling blob.sum() is passing 0 arguments

sweet flax
#

hmm.. right

tranquil jasper
#

although youre gonna hit a wall once you have to refer to the reified struct in one of its fields

sweet flax
tranquil jasper
#

yeah thats pretty much it, theyre decls that are accessed through struct instances instead of the type. but they can be reified

sweet flax
tranquil jasper
#

heres your example with comptime fields

const std = @import("std");

fn NumberBlob(comptime field_names: []const [:0]const u8) type {
  var fields: [field_names.len + 1]std.builtin.Type.StructField = undefined;
  for (field_names, fields[0..field_names.len]) |field_name, *field| {
    field.* = std.builtin.Type.StructField{
      .name = field_name,
      .type = i64,
      .default_value = null,
      .is_comptime = false,
      .alignment = @alignOf(i64),
    };
  }

  fields[fields.len - 1] = .{
    .name = "sum",
    .type = fn (anytype) i64,
    .default_value = struct {
      fn sum(self: anytype) i64 {
        return sumFunc(field_names, self);
      }
    }.sum,
    .is_comptime = true,
    .alignment = 0,
  };

  return @Type(std.builtin.Type{
    .Struct = .{
      .layout = .auto,
      .backing_integer = null,
      .fields = &fields,
      .decls = &.{},
      .is_tuple = false,
    },
  });
}

fn sumFunc(comptime field_names: []const []const u8, val: anytype) i64 {
    var res: i64 = 0;
    inline for (field_names) |field_name| {
        res += @field(val, field_name);
    }
    return res;
}

pub fn main() void {
    var blob: NumberBlob(&.{ "a", "b", "c" }) = .{
        .a = 10,
        .b = 20,
        .c = 30,
    };

    std.log.info("{}", .{blob.sum(blob)});
}

edit: comptime block isnt needed

tranquil jasper
sweet flax
#

I still don't understand the need for comptime fields. it seems like they can all be converted to decls, and all usages of them replaced with @TypeOf(instance).decl

Zig isn't a stupid language, I find it hard to believe this feature exists without reason.
what am I missing?

tranquil jasper
#

im pretty sure it's for @call, so you can have a comptime known value in a tuple. and it also is nicer than @TypeOf(v).decl

sweet flax
#

so it's for @call! I knew I was missing something!
we can debate the verbosity reason... if that was a consideration why don't we have anonymous functions?

tranquil jasper
#

from that one issue i read it was because if you could do

const add = fn(a: i32, b: i32) i32 { return a + b; };

and also

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

it'd be inconsistent, and removing the second option would be annoying. some other reasons too

sweet flax
#

welp, cannot say I argee with all of the points (Zig has anonymous types, yet still manages to give them names when some type error occurs), but I'm not too fussed. ¯_(ツ)_/¯

#

back to the original question, is there such workaround, or do I need to hold back my tears and create a wrapper type?

tranquil jasper
sweet flax
#

there is still the duplication of blob in the function call. not too bad all things considered, yet I still would like zero friction.

tranquil jasper
#

the disconnected function would probably be the cleanest, maybe just add a is_number_blob comptime field and have if (!@hasField(@TypeOf(v), "is_number_blob")) @compileError("PASS A NUMBER BLOB!!!")

sweet flax
#

the problem with the free function is that I can't use dot notation, which I really want. if the function is inherently ties to the type (as it is in this case) it should reside within the type's definition

tranquil jasper
#

i guess i can't think of anything other than those three solutions, wrapping with a really short name for inner might be your best bet lol