Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: @Result to match cast builtins inference API #16313

Open
candrewlee14 opened this issue Jul 4, 2023 · 45 comments
Open

Proposal: @Result to match cast builtins inference API #16313

candrewlee14 opened this issue Jul 4, 2023 · 45 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@candrewlee14
Copy link
Contributor

candrewlee14 commented Jul 4, 2023

Note: This is a formalized proposal based on discussion here: #5909 (comment)

Proposal started with the addition of @ResultType, like pub inline fn intCast(x: anytype) @ResultType.
It has been revised to use @Result and anytype in a function declarations, pub inline fn intCast(x: anytype) anytype

Problem: Builtin-exclusive inference API

With the recent merging of #16163, cast builtins now use an API that can't be replicated in regular userspace.
For example, take the std.fmt.parseInt function:

fn parseInt(comptime T: type, buf: []const u8, base: u8) std.fmt.ParseIntError!T

which currently has to be used like:

const foo = std.fmt.parseInt(u32, content, 10); // T must be explictly provided

compared to the usage of a cast builtin which can be used like

const bar: u32 = @intCast(x - y); // T is inferred

For the sake of consistency, it seems like parseInt should be able to be used in a similar fashion.

const bar: u32 = try std.fmt.parseInt(content, 10);

Proposal: @Result

The introduction of a builtin @Result could allow the declaration of std.fmt.parseInt to look like this:

fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!anytype {
    const T = @Result();
    ...
}

pub fn main() !void {
    const word1 = "12";
    const word2 = "42";
    const foo: u32 = try parseInt(word1, 10); // @Result is u32
    const bar: u64 = try parseInt(word2, 10); // @Result is u64 
    ...
}

Benefits

  • This democratizes this kind of inference API currently exclusive to builtins.
  • This may improve the consistency of callsites of functions with inferable types.
  • Could benefit from cast-builtin-related improvements in type inference resolution, like in the case of something like:
    // possible builtin inference improvement
    const foo: u32 = @intCast(a + b) * @intCast(c);
    // potential downstream benefit
    const bar: u32 = try parseInt(word1, 10) + try parseInt(word2, 10);
  • This also allows for the user to implement a function that looks like cast builtins from a caller's perspective, like this (via remove the destination type parameter from all cast builtins #5909 (comment)):
    pub inline fn intCast(x: anytype) anytype {
        return @intCast(x);
    }

Drawbacks

  • Allows functions with identical functionality to be defined with 2 different APIs.
    When should a user define a function with @Result inference vs. having a comptime T: type parameter?
  • ?
@candrewlee14 candrewlee14 changed the title Proposal: @ResultType to match builtin inference API Proposal: @ResultType to match cast builtins inference API Jul 4, 2023
@jayschwa jayschwa added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 4, 2023
@N00byEdge
Copy link
Contributor

N00byEdge commented Jul 4, 2023

When should a user define a function with @ResultType inference vs. having a comptime T: type parameter?

If you're returning T, never according to the new logic in zig, since you can just wrap it in an @as(T, f(...)).

One big drawback of this is that you're adding an invisible comptime parameter which silently adds more instantiations of your functions.

@andrewrk andrewrk added this to the 0.12.0 milestone Jul 4, 2023
@rohlem
Copy link
Contributor

rohlem commented Jul 4, 2023

I really like having custom casting functions - f.e. enhanced by comptime type checks specific to a use case.
With the benefits of the new inferred result type already visible in some code, I'd hate to give them up when switching from builtins to user-space functions.

In general the interface of @ResultType() providing the type of the result location to me seems minimal and sufficient, so fit for Zig.
However, the original proposal text glances over type unwrapping a bit, and I think it's a rough edge worth bringing up specifically:

The cast builtins currently already unwrap error unions and optionals: const c: error{Z}!?u8 = @intCast(@as(u16, 40)); deduces u8.
It would be easier to automatically do the same thing in @ResultType().

IMO hiding this step from the user (and discarding the additional information) would be a bit of a shame though, but I can't quite figure out how to make it work.

  • If given the full type, userspace can implement these steps manually, f.e. in a helper function, making the return type something along the lines of NonOptionalPayload(@ResultType()).
    Note that this will practically work as long as NonOptionalPayload(@ResultType()) can implicitly coerce to the actual @ResultType(), T -> E!?T will always work.
  • However, concluding from the last bullet point, the return type std.fmt.ParseIntError!@ResultType() constructed in one example would probably not work:
    No matter what R = @ResultType() we provide, the returned type E!R can never match the original R expected/deduced from the expression's result location.
    ... That is, unless result locations were propagated through (error-unwrapping if and) try expressions, and then stripped E!T -> T.

Maybe we would want both options, accessible as @ResultType() and @ResultTypeErrorUnionPayload()/@NonErrorResultType() ?
Or we choose not to provide the second first one, because behavior dependent on the callsite error set is too implicit.
Well, I can't really think of a non-confusing use case right now, so maybe it can actually be that simple.


One big drawback of this is that you're adding an invisible comptime parameter which silently adds more instantiations of your functions.

That is true; in my custom meta functions I often have a nicer interface in f that wraps an fImpl function with more verbose signature.
Especially when stripping a type (like ?T -> T) is done in userspace, you would probably want an Impl function like that to deduplicate the instantiation.
For me this is an okay approach in meta code, while in other areas it could get rather crowded.

Although for explicitness here's another idea: Instead of implicitly passing a type for @ResultType() to read from, we could instead make it an explicit parameter declaration:

fn(comptime R: type = @ResultType(), x: R) R {return x;}

Downsides:

  • We don't have assignment syntax in arguments for any other use case yet.
  • We declare a parameter slot that needs to be omitted from call sites.

The most in-line with current syntax would actually be a capture IMO:

// `keyword |capture|` has precedence in Zig
fn |R| f(x: R) R {return x;}
// could use an additional keyword
fn resulttype |R| f(x: R) R {return x;}
// (could use an operator, but no precedence of this in Zig, therefore I like it less)
fn -> |R| f(x: R) R {return x;}

// could also put it after the function name, so `fn <name>` remains Ctrl+F-/grep-/searchable
fn f |R| (x: R) R {return x;}

@Jarred-Sumner
Copy link
Contributor

Jarred-Sumner commented Jul 4, 2023

I like this idea a lot

What if it was infer or @Infer instead? One could imagine a future follow-up proposal for constraining the type to be an integer without specifying the size or implement a handful of functions like isLessThan, etc

@nektro
Copy link
Contributor

nektro commented Jul 4, 2023

related, infer keyword is proposed here #9260 too, the syntax actually pairs pretty well

@candrewlee14
Copy link
Contributor Author

candrewlee14 commented Jul 5, 2023

@Jarred-Sumner So you're thinking something like this instead?

fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!infer T {
    ...
}

The infer keyword might communicate the existence of multiple function instances better than a builtin @ResultType.

That could also be complementary if the infer syntax was already available to be used on parameters (as in the mentioned #9260).

fn foo(bar: infer T) infer K {
    ...
}

@bcrist
Copy link
Contributor

bcrist commented Jul 5, 2023

I like this proposal as is, but I can also see a slight modification:

@ResultType() seems to function basically the same as anytype for parameters, in that they both implicitly make the function polymorphic. So reusing anytype in the function signature makes sense to me. @ResultType() would often still be necessary to access the actual result type within the function, but it could be made to only be usable within a function body.

Advantages:

  • anytype is shorter and easier to read/type than @ResultType()
  • @ResultType() can be used even when the result type is specified explicitly (removes redundancy when the result type in the function signature is a complicated comptime expression)
  • anytype communicates intention better when used in a function signature (possibly subjective, but IMO it makes it clear that you need to use comptime checks if you want to limit what the result type might be, just like with anytype parameters)
  • The @ResultType() intrinsic and inferred-return-type-via-anytype could be implemented as two separate, smaller features

Disadvantages:

  • Perhaps slightly more difficult to learn - two different keywords to remember (in the context of return type inferrence; anytype already exists)

@ghost
Copy link

ghost commented Jul 5, 2023

related: #447

@N00byEdge
Copy link
Contributor

I think the anytype return type also is a valid option, but if so, @ResultType() is a terrible name and I would much rather have @ReturnType()

@AssortedFantasy
Copy link

I like @ReturnType() much better than @ResultType(). Its way clearer that the type is inferred from the function return location, wherever that may be.

@candrewlee14
Copy link
Contributor Author

Hmm, personally both feel pretty similar. What about @CallsiteType?

@rohlem
Copy link
Contributor

rohlem commented Jul 6, 2023

To me Return and Result can both be read to mean the result returned / decided by the function itself.
However, there is at least precedent for the term "Result Location Semantics" in Zig's nomenclature.

I agree CallSite (or maybe CallSiteDestination if there is some propagation through expressions like try) would be more explicit, imo preferable.
(nitpick note: Wiktionary lists both call site and callsite although the first seems preferred, Wikipedia also went with the first spelling. Langref is currently unopinionated at 5 vs 5 occurrences.)

@log0div0
Copy link

log0div0 commented Jul 8, 2023

As it was already mentioned in other discussions it's a bit cumbersome to use @as to specify the return type of an expression. For example

std.log.info("{}", .{@as(u32, try std.fmt.parseInt(content, 10))})

In the same time Zig already has a perfect way to specify the type of a constant/variable/function-argument with a colon syntax:

const x: u32 = try std.fmt.parseInt(content, 10);

It would be really nice to allow the same syntax to specify the type of an arbitrary expression (like in Julia lang, but they use double colon for this purpose). So the example above will look like this:

std.log.info("{}", .{try std.fmt.parseInt(content, 10): !u32})

Another example from here with buildins. Old syntax:

return @intCast(@as(i64, @bitCast(val)));

New syntax:

return @intCast(@bitCast(val): i64);

@mlugg
Copy link
Member

mlugg commented Jul 9, 2023

I don't have a strong opinion either way on this proposal, but I have a few notes:

  • @ReturnType would IMO be a very poor name; that sounds like something you'd use in a function body to get the return type of the function. @ResultType is a clear name which uses the lingo of RLS (rightly, since that is where this feature comes from). Alternatively, returning anytype or some infer T (if Proposal to improve the ergonomics and precision of type inference in generic functions #9260 gets in) would also be reasonable, since it would show that this works a lot like a generic parameter in that it creates a separate instantiation.
  • Builtins using an API which can't be replicated in userspace is not new or controversial, and in fact is the norm. For instance, @min/@max/@TypeOf/@compileLog are varargs, @field is an lvalue, and @import's argument must be a string literal.
  • @log0div0, if you want to seriously propose that syntax it should be a separate issue.

@N00byEdge
Copy link
Contributor

N00byEdge commented Jul 9, 2023

@mlugg

* `@ReturnType` would IMO be a very poor name; that sounds like something you'd use in a function body to _get_ the return type of the function. `@ResultType` is a clear name which uses the lingo of RLS (rightly, since that is where this feature comes from). Alternatively, returning `anytype` or some `infer T` (if [Proposal to improve the ergonomics and precision of type inference in generic functions #9260](https://github.com/ziglang/zig/issues/9260) gets in) would also be reasonable, since it would show that this works a lot like a generic parameter in that it creates a separate instantiation.

Yes, the @ReturnType() was in response to the anytype keyword for return type. that's exactly what the premise is there. It would refer to the return type of the function, not the inferred type from the call site, there would be a level of indirection where the function return type says "infer the return type" and then you're saying "use the return type, no matter if inferred or not."

If we have

fn a() anytype {
  return std.mem.zeroes(@ReturnType());
}

@kuon
Copy link
Contributor

kuon commented Jul 20, 2023

I have a mostly neutral stance on this proposal.

Just a though on the name, let's have the following code:

pub fn build_something() @ResultType() {
  var something: @ResultType() = undefined;
  switch (@typeInfo(@ResultType()) {
  ....
  }
  return something;
}

It doesn't really work with anytype in the body (same with infer T):

pub fn build_something() anytype {
  var something: anytype = undefined;
  switch (@typeInfo(anytype) {
  ....
  }
  return something;
}

It should support assignment:

pub fn build_something() @ResultType() {
  var T = @ResultType(); // comptime
  var something: T = undefined;
  switch (@typeInfo(T) {
  ....
  }
  return something;
}

From here, we could argue that the following would also make sense: (@N00byEdge suggestion)

pub fn build_something() anytype {
  var T = @ReturnType(); // comptime
  var something: T = undefined;
  switch (@typeInfo(T) {
  ....
  }
  return something;
}

Now the argument I could have for it, is easier code maintenance if we have something like:

// myfield has type IsoDate

mystruct.myfield = deserialize(IsoDate, str);

// If we change myfield type, we need to find the deserialize call and change the type too

// with proposal

mystruct.myfield = deserialize(str);

// No need to change the type here

For types that could be automatically casted, this could prevent some bugs and ensure the returned type is always exactly the type of the destination variable that the returned value is assigned to (similar to why first argument was removed from builtins). Taking this into account, I could be slightly in favor of this.

@N00byEdge
Copy link
Contributor

N00byEdge commented Jul 20, 2023

@kuon Basically agree with everything you've got here. I too am neutral on this.

Now the argument I could have for it, is easier code maintenance if we have something like:

But we have to be careful, that is an argument against it too, as mentioned before:

One big drawback of this is that you're adding an invisible comptime parameter which silently adds more instantiations of your functions.

@InKryption
Copy link
Contributor

One big drawback of this is that you're adding an invisible comptime parameter which silently adds more instantiations of your functions.

I don't think this is actually a very strong argument. Comptime arguments are already passed the same as normal parameters, meaning a function's call-site can't actually always tell us when passing a different value would incur an extra instantiation.

For example:

foo(1, 2);
foo(3, 4);

Assuming one or both of the parameters of foo could be comptime, there are potentially two generic instantiations of foo here, and you can't know that until you look at the function prototype. This is also a characteristic of inline paramters, which will make a function instantiate a runtime variant, or any corresponding comptime variant depending on the comptime-known-ness of the argument.

This feature would have the same drawback as all of existing status quo and the accepted proposal (having to read the function signature to know whether it will generate separate instantiations).

@kuon
Copy link
Contributor

kuon commented Jul 20, 2023

I agree with @InKryption that there are many places that function instance can be created transparently. I think this feature would be similar to functions accepting anytype.

On embedded platform, when I want to limit the use of some functions to reduce binary size, I work around this problem by adding comptime assertion to the type. For example, if I want a u8 and u16 generic but not signed or other size, I do something like:

pub fn something(myint: anytype) void {
    switch(@Type(myint)) {
        u8, u16 => {},
        else => @compileError("type not supported"),
    }
}

I actually have helpers functions for that, but you get the idea.

@digitalcreature
Copy link

Full support for this proposal! It would be so much more ergonomic and readable to be able to use this in userspace, such as with std.mem.zeroes() for setting default field values.

@digitalcreature
Copy link

I'd like to propose @Result() as the name of the builtin. No hungarian notation; PascalCase already indicates it is a type (see @This())

@candrewlee14 candrewlee14 changed the title Proposal: @ResultType to match cast builtins inference API Proposal: @Result to match cast builtins inference API Sep 28, 2023
@ghost
Copy link

ghost commented Sep 28, 2023

What will happen if the call site does not have a specific type, e.g. due to peer type resolution or anytype?

@nektro
Copy link
Contributor

nektro commented Sep 28, 2023

the same you'd get with the cast builtins: error: @intCast must have a known result type and note: use @as to provide explicit result type

@mohamed82008
Copy link

There seem to be a lot of suggestions here on how to signal to the compiler to infer the output type of a function. Perhaps NOT specifying any output type should just mean "infer it"? It's hard to beat 0 characters as far as syntax minimalism goes.

fn build_something() {
  var something = ..;
  return something;
}

@RossComputerGuy
Copy link
Contributor

Yeah, just inferring is the cleanest imo.

@pierrelgol
Copy link

There seem to be a lot of suggestions here on how to signal to the compiler to infer the output type of a function. Perhaps NOT specifying any output type should just mean "infer it"? It's hard to beat 0 characters as far as syntax minimalism goes.

fn build_something() {
  var something = ..;
  return something;
}

I agree, that it's a clean option, but now you've broke one of Zig's core principle of being very readable, even by people not familiar with Zig, now If a random user want to know the possible return type of the function, he/she will have to look for every places where that function is called to try to find a type, whereas an explicit @Result() or @ReturnType() is explicit enough that anyone, can understand that this functions return type is inferred depending on the call site. Maybe I'm stupid but If I try to imagine myself going into a code-base and finding function prototype that don't return anything I'd be pretty confused about what they are doing, especially If I see a return keyword.

@RossComputerGuy
Copy link
Contributor

I agree, that it's a clean option, but now you've broke one of Zig's core principle of being very readable

Wouldn't not specifying a type with const or var constitute the same thing?

@mohamed82008
Copy link

Wouldn't not specifying a type with const or var constitute the same thing?

Exactly, const a: anytype = f(..) is not more readable than const a = f(..), it's less writeable though.

@ianprime0509
Copy link
Contributor

There's an important distinction to make between the proposed @Result builtin and what many people would expect of inferring the return type of a function: the @Result builtin gives the function's result type (as determined by the function's callsite), not an inferred type based on the return expressions in the function itself. To give an example of where this might be confusing:

fn add(a: u32, b: u32) @Result() {
    return a + b;
}

const c = add(2, 2); // error: call to 'add' must have a known result type

(the error here is by analogy with existing builtins which rely on their result type: #16313 (comment))

If defining a function without any return type had this behavior, it would be very confusing for new users who might expect the return type to be inferred as u32, or who just forgot to write the return type and are then confronted with a bizarre and unexpected error at the callsite of the function (rather than at its definition).

Additionally, regardless of how the return type is written for such functions (omitted, anytype, etc.), the @Result() builtin (or something equivalent) would still be needed to access the result type from within the function body. For example, implementing parseInt using an inferred result type (as in the original proposal description) would require the use of @Result() in the function body to determine what type of integer needs to be parsed.

@mohamed82008
Copy link

My hope is that the following would work.

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

const c = add(2, 2); 

In this case, the compiler knows that add can only accept u32s as inputs so the comptime_ints at call-site will be cast to that type. The compiler also knows that add can only return u32 so when I assign its output to c, c can never (in a non-confusing world) be anything but u32. If I can do this logic in my head, then so can the compiler. Of course, the effect of this on compilation speed needs to be determined. It also gets trickier when the input to the function is anytype, comptime_int or comptime_float in which the case the output type may be ambiguous and a call-site type declaration or explicit casting may be necessary to resolve this ambiguity.

In some ways, making the above piece of code work is orthogonal to (and perhaps easier than) being able to access the inferred output type in the function body itself with @Result(). This is because if the code in the function body depends on the inferred output type and the inferred output type itself naturally depends on the code in the function body, then there is a circular dependence. This is probably a recipe for trouble.

@candrewlee14
Copy link
Contributor Author

This proposal is not really about inference of function return types by their body contents. If you have suggestions around that, that might belong in a different proposal.

The goal of this proposal is to enable callsite-inferred generics. I think it's important to keep the anytype in the function signature to signal that this is a generic function. Moving the anytype from the list of parameters to the return type in the function declaration doesn't lose readability IMO, plus it gains inference ergonomics (and arguably readability) at the callsite.
Changes to builtins in the last year allowed for more readable casts, for example, and this proposal is just about enabling those same semantics in non-builtin user code.

@expikr
Copy link
Contributor

expikr commented May 5, 2024

What if we require inferred result locations to have an identifier, and access the inferred type in the function body via @TypeOf as we currently do for arguments?

fn pythagorean(a: anytype, b: anytype) c: anytype {
    const cc = a*a + b*b;
    switch(@TypeInfo(@TypeOf(c))) {
        .ComptimeInt => {
            if (comptime isPerfectSquare(cc)) return perfect_sqrt(cc));
            @compileError("result location cannot accommodate this return type");
        },
        .Float, .ComptimeFloat => {
            return @sqrt(cc);
        },
        else => @compileError("result location cannot accommodate this return type");
    }
}

This also adds an additional benefit of enabling explicit manipulations/writes to the result location.

@rohlem
Copy link
Contributor

rohlem commented May 5, 2024

@expikr I really like that idea - it basically provides a solution for this issue as well as for related issue #2765 (turns out something similar was brought up by whatisaphone there).
The one strange case would be with blocks, where in a: b: {} the first a is the variable name and b is the block name, but the same ambiguity already exists in function arguments (recently brought up in the Discord by mlugg I think).

@expikr
Copy link
Contributor

expikr commented May 5, 2024

@rohlem in the case of return types, perhaps this visual incongruence (not really syntactical ambiguity since operator associativity establishes the strict order) would be slightly minimized by only allowing anytype returns to have an identifier? Or would that be too artificial a restriction?

@rohlem
Copy link
Contributor

rohlem commented May 5, 2024

@expikr I personally think we should allow it for all types.
It's up to the maintainers though; if special-casing it for anytype (for the time being) increases the chance it gets into the language, then I wouldn't be against it.

@djpohly
Copy link
Contributor

djpohly commented Jun 7, 2024

I have to say I'm a big fan of what this proposal would do for allocation. If we could change the Allocator interface to use the inferred result type (glossing over some error-union details here):

pub fn create(self: Allocator) Error!anytype {
    const T = @Result();
    // then same as before
}
pub fn alloc(self: Allocator, n: usize) Error!anytype {
    const T = std.meta.Child(@Result());
    // then same as before
}

then all the user needs to write is

const foo: *Foo = try alloc.create();
const bar: []Foo = try alloc.alloc(10);

which feels very clean.

@kanashimia
Copy link

kanashimia commented Jun 7, 2024

So I had this thought:
What if instead of specifying inference as a part of the function definition you could do it on the call site?
Imagine something like:

const foo: u8 = @truncate(_, value);
// or
const foo: u8 = @truncate(infer, value);
// or
const foo: u8 = label: @truncate(infer :label, value);

Not sure about the specific syntax and semantics but you get the idea.

Could be done with explicit label or implicit one, although with implicit label it isn't that obvious, implicit label inference is similar to underscore arguments for lambdas in some other languages 1 + _. Maybe labels could be generalised.

Then builtins could just take type or anytype like they did before adding result type inference to them.
It could be used on all of the already existing functions, library writers won't have to think between choosing inferred type or explicit type.
For example on allocators as was mentioned:

const foo: *Foo = try alloc.create(_);

Additionally this is extendable to literals, would help inferring array literals in some tricky cases as an example.

Downside is that callsites will be longer, especially depending on the syntax.
If explicit label is chosen and is mandatory then it would require really annoying label annotations even for trivial cases like you already do with labelled blocks.

@N00byEdge
Copy link
Contributor

N00byEdge commented Jun 9, 2024

@kanashimia

So I had this thought: What if instead of specifying inference as a part of the function definition you could do it on the call site? Imagine something like:

const foo: u8 = @truncate(_, value);
// or
const foo: u8 = @truncate(infer, value);
// or
const foo: u8 = label: @truncate(infer :label, value);

The previous syntax seems preferable to all the above:

const foo = @truncate(u8, value);

@Lzard
Copy link
Contributor

Lzard commented Jul 17, 2024

Currently, the usage of std.Build.option is:

const flag = b.option(bool, "flag", "An example flag");

It takes a type T as an argument, and it returns a ?T value; the caller needs to know that the return type will be ?T.

With this proposal, its usage could be changed to:

const flag: ?bool = b.option("flag", "An example flag");

Now it's option that needs to deduce T from the result location ?T, moving some charge off the user while making it clearer what flag's type is.

This is a simple case; I imagine there are other situations where deducing the result type from comptime arguments is more difficult, and which would benefit from this proposal as well.

@N00byEdge
Copy link
Contributor

After having thought about this for a long while, I think @Result returning ?type also could make sense, where it returns null in situations like const a = f();.

@Lking03x
Copy link

Lking03x commented Aug 5, 2024

I propose the following syntax. The @ResultType defines a const identifier (unique to the scope). This avoid having a @Result returning a ?type available in all functions even those who don't use this feature. It would be an error to not use the identifier. It can't be discarded. For any return xx, after comptime resolution, xx's type would be T or the Container(T) that matches. The type T would also be in scope in the parameters listing.

    fn parseInt(buf: []const u8, base: u8) @ResultType(T) {
        // checks if `T` is an Integer type
        // (`comptime_int` is safe since to even result to it means the function is called at comptime)
        switch (@typeInfo(T)) {
            .ErrorUnion => |U| switch (U.Child) {
                .Int, .ComptimeInt => {},
                else => @compileError("..."),
            },
            else => @compileError("..."),
        };
        // ...
        return parsed;
    }

It could be used as var a: E!u32 = parseInt();. This is a very generic parseInt. A proper signature is:

fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!@ResultType(T) {
    switch (@typeInfo(T))  {
        .Int, .ComptimeInt => {},
        else => @compileError("..."),
    };
    // ...
    return parsed;
}

T itself would only match the u32 for var a: E!u32 = parseInt(); asserting that E!u32 matches the full return type. E must be an error set containing std.fmt.ParseIntError.
For the case var a: u32 = parseInt(); a compile error would occurs as the full result can't match the return type of the function.

It will make less common @TypeOf(...) and anytype in generic function definition (and comptime T: type)

fn foo(a: T, b: T, c: ConstructNewType(T)) @ResultType(T) {}

Rather than

fn foo(a: anytype, b: anytype, c: ConstructNewType(@TypeOf(a, b))) @TypeOf(a, b) {}

For var a: u32 = try foo(); foo must return a error union, same for var a: u32 = foo() catch |e| {...} etc...
Similarly for var a: u8 = foo() orelse 778; foo must return an optional, same for if-optionals if (foo) |v| {...} etc...

@ResultType can only be called once in the function return type expression and the identifier it defines cannot be used there. It means the followings would not be legal:

  • foo(...) @ResultType(E)!@ResultType(T)
  • foo(...) ConstructType(@ResultType(T))
  • foo(...) ConstructErrorSet(T)!@ResultType(T)
  • ...

I know we are saving special characters, avoid adding new unecessary constructs... And I'm all into that but since this @ResultType(T) unlike other @ builtins, isn't a function and adds something to the scope, could it be a new construct like #ResultType(T)?

@N00byEdge
Copy link
Contributor

N00byEdge commented Aug 5, 2024

fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!@ResultType(T) {

Oh right, didn't consider the error union case. I'm not a big fan of "pattern matching" in general, this would basically be the first zig feature that behaved like this, but I'm also not sure how error unions could ever work without it behaving like that.

@Jayanky
Copy link

Jayanky commented Aug 9, 2024

I personally think the syntax is already there, it's just that Zig needs to recognize what you're doing.

A function whose return type is passed in as an argument should be able to have that type defined by the value being assigned to, similar to how a member function of a struct can have a pointer to the struct passed in with dot notation.

// definition
fn foo(comptime T: type, bar: u32) !T { ... }

// i32 passed in as type parameter
var result: i32 = try foo(5);

@N00byEdge
Copy link
Contributor

N00byEdge commented Aug 10, 2024

I personally think the syntax is already there, it's just that Zig needs to recognize what you're doing.

A function whose return type is passed in as an argument should be able to have that type defined by the value being assigned to, similar to how a member function of a struct can have a pointer to the struct passed in with dot notation.

// definition
fn foo(comptime T: type, bar: u32) !T { ... }

// i32 passed in as type parameter
var result: i32 = try foo(5);

so if I want to ignore the return type and return something completely different I do

fn foo(comptime _: type, ...) U {
  // ...
}

?

@Jayanky
Copy link

Jayanky commented Aug 10, 2024

I personally think the syntax is already there, it's just that Zig needs to recognize what you're doing.
A function whose return type is passed in as an argument should be able to have that type defined by the value being assigned to, similar to how a member function of a struct can have a pointer to the struct passed in with dot notation.

// definition
fn foo(comptime T: type, bar: u32) !T { ... }

// i32 passed in as type parameter
var result: i32 = try foo(5);

so if I want to ignore the return type and return something completely different I do

fn foo(comptime _: type, ...) U {
  // ...
}

?

A function whose "return type is passed in as an argument" isn't just any function with a type parameter. Specifically, it's a function where the type specified in the parameter is the type that gets returned.

In your example, the ignored parameter is not the type of the return, so it will stay as an explicit argument.

@Des-Nerger
Copy link
Contributor

Des-Nerger commented Jan 1, 2025

@expikr: in the case of return types, perhaps this visual incongruence (not really syntactical ambiguity since operator associativity establishes the strict order) would be slightly minimized by only allowing anytype returns to have an identifier? Or would that be too artificial a restriction?

Why not just make the parentheses around the var_id: type pair mandatory, like Golang does?:

fn pythagorean(a: anytype, b: anytype) (c: anytype) { ... @TypeOf(c) ... }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests