6 ms·
One thing that I repeatedly bump into in Rust is the oddity of multi-argument functions. To explain the problem, let's look at another language for a moment: in
by yakubin 5y ago
One thing that I repeatedly bump into in Rust is the oddity of multi-argument functions. To explain the problem, let's look at another language for a moment: in SML there is no such thing as multi-argument functions per se. There are two ways to write them anyway:
* the function's argument is a tuple (this is the more common way, not only in SML, but also how it's done most commonly in maths);
* currying.
I'm not a fan of currying, because it singles out the first argument over the others. However, the first approach (tuples) is very ergonomic, because now you can pass the return value of one function as an argument to a "multi-argument" function. One application of that is function composition. But another is a simple map. I've lost count of how many times in Rust I had an "o: Option<(A, B)>" and a "f: fn(A, B) -> C" and couldn't call "o.map(f)"; instead the programmer is forced to write more noisy code with lambdas. And lambdas sometimes don't play well with the borrow checker for mysterious reasons (even those that don't capture anything).
Also, I think it would be nice if the compiler generated named discriminants for every enum. Currently, "std::mem::discriminant" returns an opaque "std::mem::Discriminant<T>" (which doesn't have a name). In order to compare discriminants I need to create a full-blown enum value and extract the discriminant from it later. I've worked around this problem by using the "strum" crate, which has a macro to produce another (plain) enum, but having the discriminant type be named "std::mem::Discriminant<T>" (to signal the connection to the original enum) and have named variants at the same time would be the best of both worlds.
EDIT: If I knew more about possible breakage caused by my first suggestion, I could probably start working on an RFC. However, I feel I don't have enough knowledge about Rust to anticipate that.
For my second suggestion I think I'm going to try to propose that. I'm reading the docs on the process now.
- nynx 5y agoHaving enum variants be structs has been proposed, and that could solve your issue if `mem::disciminant<Enum::Variant>` returned what you wanted.
- yakubin 5y ago> Having enum variants be structs has been proposed That's great news. > that could solve your issue if `mem::disciminant<Enum::Variant>` returned what you wanted. I think you misunderstood, or I explained it poorly. Given an: enum Enum { A(int), B, } I would like mem::Discriminant<Enum> be the same type as if there was such a definition in code (illegal in Rust, because Rust doesn't have specializations): enum mem::Discriminant<Enum> { A, B, }
- nynx 5y agoAh, I did misunderstand. That’s an interesting idea.
- Arnavion 5y ago>I've lost count of how many times in Rust I had an "o: Option<(A, B)>" and a "f: fn(A, B) -> C" and couldn't call "o.map(f)" Non-serious solution: #![feature(fn_traits, unboxed_closures)] pub fn foo<Args, C>(o: Option<Args>, f: impl FnOnce<Args, Output = C>) -> Option<C> { o.map(|v| f.call_once(v)) } https://doc.rust-lang.org/stable/std/ops/trait.FnOnce.html https://doc.rust-lang.org/stable/std/ops/trait.FnOnce.html "Non-serious" because it won't be stable any time soon, and it's not as concise as `o.map(f)` anyway. More seriously, I don't think `o.map(f)` will ever happen, because it'll be backward-incompatible in the case of `o: Option<(T,)>` and `f: <U>impl FnOnce(U)` - `U` used to be `(T,)` and now would be `T`. Something like `o.map(spread(f))` might work if vararg generics are added to allow a generic `spread` that works for any tuple and matching-arity FnOnce.
- yakubin 5y ago[EDIT: comment applicable only to an older version of the parent comment] This "foo" still suffers from the same problem as "Option::map" does: it is hard-coded for functions of fixed arity (2 in this case, 1 in Option::map's case). Needing to write this function doesn't shorten the code. The SML-inspired solution allows one to write an "Option::map" which accepts functions of all possible arities (since they are all 1). But yes, if there are a lot of places where I want to call "Option::map" for an "f" which takes 2 arguments, that could work. Although, personally, I think I'd still opt for an explicit lambda, in order to communicate intent more directly (avoid one level of indirection).
- brabel 5y ago> the function's argument is a tuple (this is the more common way, not only in SML, but also how it's done most commonly in maths); The Ceylon programming language worked that way[1] and it was really cool to be able just what you wanted: `o.map(f)`. It's a shame that other languages didn't seem to pick up this idea... I've no idea if this could be made to work in Rust though (given Rust has no GC and it would go against its philosophy to allocate parameters like this on the heap just to create tuples - maybe the compiler could get rid of the actual tuples altogether?). [1] https://ceylon-lang.org/blog/2012/12/21/tuples-and-functions/ https://ceylon-lang.org/blog/2012/12/21/tuples-and-functions...
- estebank 5y agoRust tuples are on the stack, to have them on the heap you need to explicitly use Box. Funnily enough, internally this is how closures parameters are represented internally, as a single argument that is a tuple of all its parameters.
- _nalply 5y agoPerhaps you meant that variables which are closed over are in an anonymous struct or in a tuple. I am fuzzy about the details but as far as I know parameters are passed like as in other functions.
- estebank 5y agoRust represents closures in the following way: pub trait Fn<Args> { type Output; extern "rust-call" fn call(self, args: Args) -> Self::Output; } where Args is a tuple of all the actual arguments. You used to be able to cause an ICE on nightly if you used something other than the correct argument[1], but instead it now tells you that it should have been a tuple: error: functions with the "rust-call" ABI must take a single non-self argument that is a tuple --> src/main.rs:7:5 | 7 | extern "rust-call" fn call_once(self, args: i32) -> Self::Output {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0059]: cannot use call notation; the first type parameter for the function trait is neither a tuple nor unit --> src/main.rs:12:5 | 12 | x(42); | ^^^^^ [1]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=9b66c97982c370bc865597301af2ae7e https://play.rust-lang.org/?version=nightly&mode=debug&editi...
- kaszanka 5y ago> the "strum" crate, which has a macro to produce another (plain) enum, but having the discriminant type be named "std::mem::Discriminant<T>" I don't understand, is this a copy/paste mistake?
- yakubin 5y agoYou cut out the rest of the sentence, which is needed to make sense of it. What I mean is, with the "strum" crate I can use a proc macro to automatically generate an "EnumDiscriminant" enum for an "Enum" enum, where "Enum" is defined as: enum Enum { A(u32), B, } and "EnumDiscriminant" is defined as: enum EnumDiscriminant { A, B, } However, the only thing linking those two is a naming convention. I would like to have a universal name for such discriminants. The Rust standard library already has such a discriminant type for any enum - mem::Discriminant<T>, where T is the enum type. However, in order to get the discriminant of Enum::A(5) I need to write mem::discriminant(Enum::A(5)), which introduces noise (especially with bigger tuples or structs) - this 5 here is irrelevant. The mem::discriminant() function is useful for contexts where I have a variable of type Enum and want to get its discriminant, but when I just want to pick a particular discriminant, it's too noisy. I would like to be able to name the discriminant directly. I.e. I would like the standard library type Discriminant<T> be defined for each enum the same way EnumDiscriminant above is defined for Enum. So I could just write Discriminant::<Enum>::A.
- swfsql 5y agoI also dislike not being able to coerce tuples into arguments of a function. To explore a little about this, and also learn proc-macros, I made https://github.com/swfsql/loosen https://github.com/swfsql/loosen - but I've never really used it.