Generic types and source file structs

Relevant doc here and here and, from an earlier thread, the maybe-zen: “avoid repetitive namespacing” (like foo.bar.bar, but maybe even foo.bar.Bar - debatable).

So, for my generic structs, I’ve been using:

fn Baz(comptime T: type) type {
    return struct {

Great. No problem. But I confess that I like the source file struct motif, especially because I think I’d like to break my structs into files of their own anyway, for this project.

If they weren’t generics, this would be a no-brainer, and I’d be able to avoid repetition in namespace (like foo.bar.Bar), as well, because Bar.zig would be a file, rather than bar.zig having a struct named Bar.

But… how… with generics? I know it’s pretty idiomatic to cap-name the function if it’s a function that returns type (as above, Baz), but I’m wondering about a different approach, such as:

foo/
  bar.zig

where bar.zig contains a lowercase function, factory-ish, but to generate the specific type (not shunt to creating an object simultaneously). So:

//bar.zig
fn makeType(comptime T: type) type {
    return struct {
//...

//main.zig
const Bar = bar.makeType(u8);
     // for some simple app that only needs one kind of Bar

// or, more likely, for an app that needs variants:
const TransmitterBar = bar.makeType(Zag);
const ReceiverBar = bar.makeType(Zug);

Note, this Bar is not intended to be super general-purpose, like std data structures - these generic structs are pretty specific to the domain, so there’s slightly less incentive to be bound to do it “the std/general way”, in every regard.

I should also say: when I have these cases where I want to “make” a specific type, like Bar(u8), and const-NAME it, I don’t love naming it BarT or BarType or or even BarU8 (though this is what I often do currently)… this only crops up in the top usage, above, where the program (or whatever) really just thinks of this as “this program’s Bar”, so a specific name like TransmitterBar doesn’t lend itself naturally. IF I use a lowerCaseFunction() maker, I can just name the ultimate type Bar, as there’s no conflict. (I know, another way to do that would be const Bar = foo.bar.Bar(u8) or something of the sort, and just make a local version of the name, but even though it’s scoped, I don’t love this.)

So, 1) is the above proposal heinous for some reason I’m overlooking?, 2) is there another way I should consider - a way that might not be as common, but is clever to my hopes and possibly more idiomatic?

Yes. The compiler assigns a name to types, this scheme would leave makeType(u8) as the primary name of your returned type. The identifier it’s assigned to plays no role, unlike when a type is constructed directly.

So unless you can stand looking at foo.bar.makeType(u8) and foo.baz.makeType(usize), I suggest not doing this.

I think I COULD stand looking at BarA = bar.makeType(u8) and BarB = bar.makeType(Bomb)- that actually feels very comfortable to me, and I guess, from what you’re saying, that would wind up embedding (in that name the compiler assigns) “bar” for analysis. Right? (I would only choose to write out foo.bar.makeType() if there was another, like baz.bar, that followed the same pattern, such that “bar” would not be sufficient to tell them apart. Perhaps, for example: const v1Bar = vendor1.bar.message.makeType(u8) and const v2Bar = vendor2.bar.message.makeType(u8)- in this case I might not care about the “bar”ness of the two, in my CODE, though, as you say, the compiler would assign values that include the “bar”, and that would be fine. I should also indicate that my supposed use is kind-of “once at the top”, for app setup, and I’m always assigning the new type to a name of its own that is sufficiently useful for my context (even forgetting it was a generic construction thereafter). From there on out, twenty variables may get the type v1Bar, but we’d never see the vendor1.bar.messagechain again, after the initial type-fabrication. All this detail change anything?

But the point is excellent - I’m thankful for the reminder.

1 Like

That’s the thing, you would. That is the actual name of the type. it’s what @typeName gives you, it’s what compiler errors use, it’s what ZLS will show.

Which is why you don’t want to do this.

Sorry, I miscommunicated. I meant that the source code would not include lots of vendor1.bar.message references - I meant “we’d never see” that in the source code. I appreciate that it would be everywhere in symbols, and that’d be fine (even helpful… even for ZLS). If the sourcecode “cleanly” just had my defined name, V1Bar, for the few (or many?) instances of V1Bar, then that would(?) “be fine” with me. But the thing you may be saying is: “nope, don’t do that. you want vender1.bar.message repeated everywhere if that’s really what the thing is (or, actually, vendor1.bar.message.Message(u8)or whatever) - don’t go defining your own types to “simplify” that to V1Bar; you’ll regret it.” If so, that’s the conclusion I’d have to evaluate and come to understand more fully.

And, to clarify, based on what you said, I would NOT do this:

const foo = @import("foo");
const bar = foo.bar;
const makeType = bar.makeType;

const Bar1 = makeType(Zag); // bad!  Even possible?!
const Bar2 = makeType(Zug); // bad!  Even possible?!

// instead, I'd always do this:
const Bar3 = bar.makeType(Zag); // now "bar" is in the symbol name
const Bar4 = bar.makeType(Zug);

// note...
const makeType = hoo.makeType; // can't even do this, anyway
const Hoo1 = makeType(Zag); // if it was possible, this would be
                            // indifferentiable, symbolically, from Bar1,
                            // even though I assign it to a name Hoo1
                            // (i.e., that doesn't matter, as you point out)

Why don’t you run a few experiments, and see what @typeName says?

It will clarify your understanding better than anything I could say at this point.

1 Like

Yes, have done. Thank you for your patience with my interest in pushing a thing. So, I’m actually ok with what I see. E.g., if I force a compile error, the compiler refers to “foo.makeType(u8)”. Perhaps what you’re suggesting is: “that’s ugly”… and doesn’t even look “type”ish. But it does tell me that it’s “a foo of u8”. It even tells me how it was made, basically, which I could get used to.

But, for posterity, to spell out the motif, here’s foo.zig:

pub fn makeType(comptime T: type) type {
   return struct {
      ig: T,
   };
}

And here’s test:

const foo = @import("foo.zig");
test "makeType" {
   const FooA = foo.makeType(u8);
   const FooB = foo.makeType(u16);
   trace("FooA is a {s}, FooB is a {s}\n", .{ @typeName(FooA), @typeName(FooB) });
   const fooA: FooA = .{ .ig = 8 };
   const fooB: FooB = .{ .ig = 65_000 };
   trace("fooA's type is a {s}, fooB's type is a {s}\n", .{ @typeName(@TypeOf(fooA)), @typeName(@TypeOf(fooB)) });
}

this prints:

FooA is a foo.makeType(u8), FooB is a foo.makeType(u16)
fooA's type is a foo.makeType(u8), fooB's type is a foo.makeType(u16)

I (think I) appreciate that this might look awful to many… but is it just a bit out of the box?

Or, getting back to the original hope: is there a (different) great way to do what I want to do? (That is, source-file-struct motif with generic types, that don’t require foo.Foo and, bonus, that don’t eat the name Foo, itself, leaving that name available as a possible fully-realized type name. That last bit probably feels the weirdest: “why would anyone care about that?” and that’s a little hard to motivate without more detail on what I want to do.)

1 Like

If you can live with literally every function-specialized type having the same name: makeType, then that’s up to you.

There is not.

Zig style uses PascalCase for types, and for functions which return types. This is why.

You don’t have to do things this way, of course. You should, but you don’t have to.

The way it’s done in the standard library is like this:

pub const MultiArrayList = @import("multi_array_list.zig").MultiArrayList;

I find this tolerable, but not really preferable. As in, I don’t like it, but I don’t really see any better alternatives.

3 Likes

Ah! Maybe I’ve missed something. My snippet indicates not the name makeType, but the name foo.makeType(u8)- indicating “foo”ness as well as u8 specialization (to differentiate from other structs and other type-specializations, like foo.makeType(u16) – have I missed something? I definitely would not like it if the symbol was simply makeType, true.

Or makeType for short.

1 Like

I’d noticed, and that does contract a little; I can appreciate this. Whether getting my (properly-cased, but not-yet-specialized) Foo this way or with the more typical two-liner, I still have the task of: const FooT = Foo(u8) (or const FooU8 = Foo(u8)) - but such is at least nice and clear, so….

Thank you all.

Do you mean that in some contexts (like ZLS? or debuggers?) it might get stripped down to just makeType? (I don’t have either set up right now, so can’t check.) Yes, I wouldn’t want that; that would definitely be the nail in the coffin.

Yes, that is the main reason we (at least I) don’t do what you are suggesting.

It’s something I ran into recently, in (master branch) std.Io.File.MultiReader there is a function for getting the std.Io.Reader interface of one of the files, but another function for getting the std.Io.File.Reader. In the autocomplete/in editor docs they look like they are returning the same type since zls only shows their local name Reader, not the fully qualified name, not even how they are referred to in the source which necessarily has to differentiate them.
It only took 30 seconds to get around my confusion, but it was annoying.

Debuggers tend to use fully qualified type names, but it depends on the debugger and can probably be configured, you also probably are already familiar with the source. But it’d still be annoying.

2 Likes

I think that’s a deficiency of the Zig LSP. I do see the full name in the Rust LSP.

The rust LSP is not relevant, rust has a different type system, it is also a more stable language which allows the LSP developers time to implement more features.

I agree it’s a deficiency of zls, but zig is an unstable language, compiler and std, the zls devs simply have to spend a lot of time keeping up. They do add features and improve existing ones, its just slower due to zigs unstable state.

I still think your naming is needlessly confusing, yes you can get used to it, and it’s your code, I won’t judge what you do for yourself.

But I assert that it takes more effort to know what the type is, you have to look at where it’s coming from, and if you’re looking at the source you need to be more aware of where you are in it.

I do judge your recommending for others to do.

Oh, I was not attacking or even criticizing Zig. Just trying to identify where the problem is.

I think this was intended for the OP.

I didn’t interpret it as an attack, I was explaining the problem, not defending/counter attacking :slight_smile:

oops, I haven’t been here recently and there is not an indicator who OP is without scrolling to the top.

Ah, I see, thank you. Yes, think this is worth avoiding for the several reasons offered. I’ll summarize an official answer.

Thank you all, again.