-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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: Anonymous function literals with function signature inference #4170
Comments
We should make the syntax easy to use for refactoring code. Inspiration: https://github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md#code-refactoring Step 1 /// inline refactoring
const std = @import("std");
const V = struct {
i: i32,
u: i32,
};
pub fn main() !void {
const v = V{.i=42, .u=35};
std.log.debug("{}", .{v.i});
} Step 2 pub fn main() !void {
const v = V{.i=42, .u=35};
{
std.log.debug("{}", .{v.i});
}
} Step 3 (proposed syntax for block with restricted access to outer scope) pub fn main() !void {
const v = V{.i=42, .u=35};
bind (v) {
std.log.debug("{}", .{v.i});
} ();
} Not sure how this syntax should be. One orthogonal syntax I can think of (refer to #10458) is too complicated. Step 4 (call inline anonymous function) pub fn main() !void {
const v = V{.i=42, .u=35};
fn (v: V) void {
std.log.debug("{}", .{v.i});
} (v);
} Step 5 (extract function) const foo = fn (v: V) void {
std.log.debug("{}", .{v.i});
};
pub fn main() !void {
const v = V{.i=42, .u=35};
foo(v);
} |
const foo = fn () void {}; This aligns with const foo = fn () void {
const bar = fn () void {};
}; To be honest, I want to deprecate |
@locriacyber see #1717 |
I've tried to look into Also, I'm not sure what the syntax should be. Closest to current syntax:
Separate type and implementation (like struct literal):
|
No effort has been made since this proposal has not been accepted and any work on it would likely end up being rejected. |
I think this proposal is a bit broad but a lower powered version would greatly improve common refactoring issues. pub fn main() void {
const a = A.init();
defer a.deinit();
const b = B.init();
defer b.deinit();
...
const z = Z.init();
defer z.deinit();
const e1 = a.foo() + b.foo() + ... + z.foo() + 10;
const e2 = a.foo() + b.foo() + ... + z.foo() + 100;
} Status quo, extract a structconst ExtraStruct = struct {
a: A,
b: B,
...
z: Z,
fn extracted(self: ExtraStruct, param: usize) usize {
return self.a.foo() + self.b.foo() + ... + self.z.foo() + param;
}
};
pub fn main() void {
const a = A.init();
defer a.deinit();
const b = B.init();
defer b.deinit();
...
const z = Z.init();
defer z.deinit();
const extra_struct = ExtraStruct{
.a = a,
.b = b,
...
.z = z,
f};
const e1 = extra_struct.extracted(10);
const e2 = extra_struct.extracted(100);
} Extracting a struct is very annoying. This proposal can help but we don't need its full power. Instead of an anonymous function, all that is needed here is an inlined parameterized block. Proposed, inline parameterized blockpub fn main() void {
const a = A.init();
defer a.deinit();
const b = B.init();
defer b.deinit();
...
const z = Z.init();
defer z.deinit();
const extracted = inline blk: |param| {
break :blk a.foo() + b.foo() + ... + z.foo() + param;
};
const e1 = extracted(10);
const e2 = extracted(100);
} The idea is since Note that since However, since it is a lexically scoped block, I would make it a compile error if it is passed like a closure. fn foreach(self: @This(), closure: anytype) void {
for (self.buckets) |bucket| {
var current = bucket.first;
while (current) |node| : (current = node.next) {
closure(&node.key, &node.value);
}
}
}
fn countValues(self: @This(), value: Value) usize {
var count: usize = 0;
const closure = inline |_, v| {
if (v.* == value) count += 1;
};
self.foreach(closure); // compile error: `closure` not allowed to escape
return count;
} This mean it wouldn't work for #6965, where this example is adapted from. If it were allowed to escape, it would violate zig's principle of "no hidden control flow" if the block |
@Pyrolistical's simplified version of this proposal alone would be handy, but with a minor tweak this might sensibly solve the open question of how Zig can wrangle the semantics of C's function-like macros: allow passing these to inline functions only. An example that has personally burned me a few times (Pipewire's POD macros) is a procedural-style tree builder pattern used in C. The idea is to make a builder for the structure that appends new nodes to a pre-allocated buffer with a stack to keep track of hierarchy, but then abstract away the push and pop to a macro that takes arbitrary expressions as an argument. The most valuable thing about this pattern is not the abstraction, but that it puts the hierarchy you're creating front-and-center, which can vastly improve readability when that information is key. Compare these two, roughly modeled after Clay (nicbarker/clay#3): OpenContainer(containerConfig); // Make container element and push it to a hierarchy stack.
PlainChild(config); // Make other elements, which automatically parent
PlainChild(config); // to the container at the top of the stack.
OpenContainer(containerConfig); // Start 2nd level, also parented to the previous top of the stack.
CompositeChild(config); // Hoisted a section of the tree to a function, now we have a new building block!
CloseContainer(); // Finalize 2nd level and pop it from the stack.
for (int i = 0; i < tailLen; i++) {
PlainChild(tail[i]);
}
CloseContainer(); // and again for the 1st. #define CONTAINER(config, children)
OpenContainer(config);
children
CloseContainer();
...
CONTAINER(containerConfig, {
PlainChild(config);
PlainChild(config);
CONTAINER(containerConfig, {
CompositeChild(config);
});
for (int i = 0; i < tailLen; i++) {
PlainChild(tail[i]);
}
}); This type of abstraction is unrepresentable in status-quo Zig as far as I can tell. I'd argue that it fits very well with the ethos of making the most correct and performant approach the easiest, and that the degree it hides control flow is no less than that of async functions (not a good omen, to be fair) or labeled breaks. inline fn CONTAINER(config: ContainerConfig, children: inline fn (void) void) void {
OpenContainer(config);
children();
CloseContainer();
}
...
CONTAINER(containerConfig, inline {
PlainChild(config);
PlainChild(config);
CONTAINER(containerConfig, inline {
CompositeChild(config);
});
for (tail) |tailConfig| {
PlainChild(tailConfig);
}
}); AlternativesThese come with a pretty big caveat: none of these can be done by translate-c, as the semantics of calling must change to break through a layer of abstraction, so manual wrapping of the C library is required. Struct-based approachDespite being the most straight forward seeming way to do this, it is not a zero-cost abstraction. Items were arranged merely by the sequential nature of execution previously, but now values must be returned so items can be accounted and re-combined in order. Additionally, this complicates inclusion of control flow, as you are now working with manipulating data rather than just writing statements. CONTAINER(containerConfig, blk: {
var children: []Element = .{
PlainChild(config),
PlainChild(config),
CONTAINER(containerConfig, .{
CompositeChild(config), // This must now only add a single element or wrap them in a container!
}),
};
for (tail) |tailConfig| {
children = children ++ .{ PlainChild(tailConfig) };
}
break :blk children;
}); Defer and blocksThis is probably the biggest argument against this justification, as it is very explicit about the execution without being unreadable, but is less so about the intent. However, if the push and pop/finalize are non-trivial, this can become untenable without adding extra abstraction elsewhere. {
OpenContainer(containerConfig); defer CloseContainer();
PlainChild(config);
PlainChild(config);
{
OpenContainer(containerConfig); defer CloseContainer();
CompositeChild(config);
}
for (tail) |tailConfig| {
PlainChild(tailConfig);
}
}
|
Rationale
In the discussion of '#1717 function expressions' there are a lot of comments that touch a topic of function signature inference:
#1717 (comment) #1717 (comment) #1717 (comment) #1717 (comment). Which is a natural use case for anonymous function expressions (classis example is an argument to 'sort'), however as stated by @hryx #1717 (comment) this is not a goal of that proposal, so this use case will have to be solved anyway, so thats why i'm creating this proposal.
This proposal is compatible with #1717 (which is 'accepted' at the time of writing) but in the substance makes it a bit redundant leaving the controversial [1] [2] 'syntactic consistency among all statements which bind something to an identifier' as the main difference.
Closures are non-goal.
The proposal
Add an ability to declare a function inside of an expression. The types of the arguments of the function should be infered from the context, the same applies to the return type.
Possible syntax:
An opening dot is a convention established by anon enum literals, structs, etc.
Parameters enclosed by
| |
instead of parentheses is also present in the language (eg. loops).Such expressions should be coercable to the function type that is expected in the declaring scope.
Ambiguous expressions should probably be compile errors:
Some examples:
Expression expressions
Naming these gets complicated:
Basically a shortcut for one line function that return some expression. These are not part of this proposal.
The text was updated successfully, but these errors were encountered: