See: https://www.reddit.com/r/Zig/comments/1pf47kg/idea_pipe_operator/
Edit: fix the link
See: https://www.reddit.com/r/Zig/comments/1pf47kg/idea_pipe_operator/
Edit: fix the link
Iâm not a reddit user, so maybe I donât get it, but the link just takes me to the zig reddit page.
My initial reaction to this is that it doesnât really make sense for Zig. Donât get me wrong, I love the pipe operator, (big nushell fan), but it opens a serious can of worms.
First of all, if you introduce a pipe operator, youâll inevitably also need to introduce a protected variable name to represent the piped content, for times when you need to reference it by name, something like it or in.
Example: Produces <foo> in nushell:
'foo' | $'<($in)>'
A more illustrious example, albeit not real code:
'foo,bar' | str split ',' $in
Adding one operator and locking down one variable name doesnât sound like much of a burden, but in a super streamlined language like Zig, every character counts.
More importantly, the pipe operator exists to make functional programming easier. Functional programming, as sexy as it can be (yes I do like haskell as well), is not really conducive to low-level code, and Zig has taken a position that while FP should be possible, friction has been put in place to make you think twice about whether or not youâre making the right choice.
If you donât believe me, try making a higher order function in Zig - itâs possible, but you can practically hear the compilerâs disapproving look ![]()
I think this is the Link they were trying to reference: https://www.reddit.com/r/Zig/comments/1pf47kg/idea_pipe_operator/
OP, please correct me if Iâm wrong
Yeah thatâs the correct one.
const y = val |> @intCast |> @as(u32, _);
âŚthis doesnât look any better than the nested version tbh. The problem is the required casting noise, not some other syntax details. Also instead of a new operator it could just as well be handled via the dot-operator if Zig had UFCS support:
const y = val.@intCast().@as(u32);
This kind of thing will probably never be added to zig because it is minimalistc to a fault.
I personally donât think that âjust use variablesâ is the best for every situation, but even if youâd try to argue for that you will just hit âAvoid local maximumsâ from zig zen. Variables are technically good enough for everything.
The really nice thing about piping though is that it can make nested expressions read left-to-right, which is just much nicer to read in a lot of situations. I do like it in languages that support it.
I would really like if Zig would have UFCS.
Right now the exists some kind of duality between functions. Those that are defined in the scope of the type of the first parameter and others that arenât.
The first ones can be called by foo.bar() while the the others canât. This also means that (without changing the source) you canât extent a type with a function and just call it ânormallyâ.
One case where I usually have this when creating functions that act on on some kind of ArrayList (or the slice) or other std type.
The main Argument against UFCS I see is that you canât just read struct definition and be sure that those are all the functions, that can be called with foo.bar(). Meaning it hurts explicitness. But this is easily solved by modern tooling.
Please define the term âUFCSâ for me.
I think the devil is in the details with UFCS. For one, Zig would need to support ADL to support it, which, given C++âs track record in this area, probably introduces more complexity to the compiler than is worth it. Take this currently valid code for example:
const foo: Foo = .init();
Without UFCS, this is unambiguously a call to the function member Foo.init. But with UFCS, it could be some other non-namespaced init with an anytype as the first parameter.
I believe UFCS has been shutdown in the C++ committee for similar reasonings. Chances are, if C++ committee thinks it is too complex, it is probably best avoided.
Here is a link for the wikipedia site
The paper I had in mind.
Thanks for the references. Turns all function call syntax into method-call syntax. C# does this, called âextension methodsâ (IIRC - stopped writing C# when I retired).
Back in the days (before 0.11, I think), @intCast() accepts a type as an argument. Bringing that back under a different name (@intCastAs()?) seems a more straight-forward solution. For pointer casting, I wonder if it wouldnât make more sense to make @ptrCast() accept an options argument in lieu of @alignCast() and @constCast(). So something like this:
const a_ptr: *A = @ptrCast(opaque_ptr, .{ .@"align" = true, .@"const" = false });
The Reddit OP is saying this would make the ânested casting less annoyingâ. Are people really casting things that often?
If youâre working with C libraries I can see you might need a layer of type management around the calls. Thatâs why C pointer coerce so easily. However, whilst youâre in one language, casts are a code smell in my book. Itâs a sign youâve chosen the wrong types. Youâve got types encoding information which youâre quite happy losing in the cast (e.g. signedness, constness, a pointerâs domain). So if youâre happy losing that information, why use that type?
I think casts is quite important. I believe that explicit type conversions help write safer code, while Zig uses implicit conversions in some places (for example, converting from a lower bit number to a higher bit number), which can introduce some potential footguns:
In addition, using enums and packed structs helps to isolate integers of the same length but with different meanings, which improves type safety. However, since these types do not support native operations, a lot of type conversion code, such as @enumFromInt, also needs to be written to handle them. Furthermore, because T{} is discouraged, more and more @as(T, .{}) will appear in the code.
Although there are some scalability issues, the current approach is logically consistent in Zig. Zig unifies types and namespaces, so there is no need to maintain both the âvirtualâ namespace of a class and the actual namespace as in C++. ADL in C++ is a disaster, making it difficult for developers to determine which function they are calling just by looking at a single namespace.
One acceptable approach I can think of is a âdeclaration extend typeâ, which constructs a new type based on an existing type, ensuring its ABI is consistent with the base type. It inherits the existing declarations of the base type, prohibits adding new fields, allows adding new declarations but forbids overloading. When used, the extend type can be type coerced with the base type (not between different extend types of the same base type, but conversion can be indirectly achieved through the base type).
However, in practice, a similar effect can already be achieved with single-field wrapper structs, so the approach I thought of may not be worth adding.
The pipe operator gets around much of the implicitness of the extension type / UFCS stuff - the presence of the pipe indicates the externality of the function while still allowing it to be placed to the right of its first argument.
The syntax definitely takes getting used to, but it can make linear operation chains easier to read by converting nested operations into a sequence of operations.
Realistically, the current ways of doing things are either operation nesting (good for tree-like operations, but linear chains of operations read right to left), or several one-off intermediate variables (possibly in an anonymous scope; naming these isnât always easy, especially if itâs conceptually the same data, just going through a sequence of transformations).
Single field wrappers are similarly explicit to pipes, but have more syntactic âinertiaâ than a pipe: youâll generally have to unwrap/rewrap each intermediate value before and after every external function call, with no additional explicitness beyond what a pipe operator offers.
Totally agree. Type conversions do need to be explicit when they have potential consequences (e.g. losing information). My point was that type conversions shouldnât be needed very often. If youâre constantly switching types youâre probably using the wrong type.
IME yes, as soon as you actually use Zigâs flexible-width integer type, or need to mix signed and unsigned integers itâs casting galore. If you only ever use usize itâs mostly fine though.
I wrote about that a bit here in my home-computer-emulator blogpost (search for Bit Twiddling and Integer Math can be awkward). Many of the required casts could be inferred by the compiler though without giving up the idea that Zig doesnât do any implicit casting that would lose information.