-
Notifications
You must be signed in to change notification settings - Fork 1.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
Infix notation for function call / invocation. #1579
Comments
This is a TIMTOWTDI feature with very little (any?) gain. It doesn't read terribly well either. |
Also discussed in: rust-lang/rust#8824 |
TIMTOWTDI = There's more than one way to do it There's nothing inherently wrong with having more ways to do one thing. |
The idea is to introduce one more operator to end all custom operators. Instead of defining custom operators, we can just use functions for this purpose. I know the argument that methods are infix,
|
It doesn't seem beyond the wit of man that someone might reasonably expect a library-defined operator whose return type is the same as that of the first operand to be usable as an augmented assignment. How easy is that to do? |
Discussion on fixity from IRC, for future reference...:
|
I like custom infix operator as long as limited in ascii letter. But I don't like if you can turn any arbitrary function to infix call. When I create function see previous discussion in https://users.rust-lang.org/t/infix-functions/4724 |
Does it interact with generics? Would we end up with |
@durka Probably, you'd have to specify it somehow with generics, and what you've come up with seems best. But I guess you'd save |
I don't want this in the language. I think it adds a lot of complication, and it's unimportant to me personally. However, I wouldn't be too opposed to this. It seems nice, especially, for mathematical code. |
Infix functions are great in languages like Haskell where operators are more common, but it doesn't seem to give Rust much. |
Are there examples of projects where this would result in a decent ergonomic improvement? |
@AndrewBrinker I venture a guess that writing mathematics libraries, e.g: linear algebra, and using them, as well as the low level parts of physics engines might benefit from the increased ergonomics. There might be other projects of course, but off of my head, that's the typical use case. |
@Centril The existing overloadable operators covers 95% of those use cases. |
Would this mean we could write obfuscated code that compiled both in Rust and TeX? ;) I love the infix operator notation in Haskell, partially because not all mathematical operators to *, +, etc. even when mathematicians write them that way. I love Haskell's lack of parentheses on function calls too though. Yet, Rust is a rather different language, so tools like currying do not always fit. At first blush, I'm thinking one should start with just knowing when to avoid, or argue against, using object I fear this proposal makes the left vs right action issue worse. In other words, In the long run, one could probably build a strict functional language with Haskell-like syntax, maybe an Idris fork, that compiles to Rust code, but does not introduce a garbage collector. I'd suspect that's a better way to build DSLs that compile to Rust. |
It strikes me that, although it wouldn't be super pretty, procedural macros could be used to provide as flexible a mathematical notation as is desired, without the introduction of infix function application. Actually, looking at the IRC log @Centril includes above, it seems this idea has been mentioned previously. It would be some extra work, and would be strictly-speaking less flexible than infix function application, but it is something that would work today without changes to the language. |
I was going to post this in #818 but it's more appropriate here. I'm in favour of infix functions. User definable operators can be a bit tough to wrap your head around depending on how they're defined, but simpler, more rigorous operators like -- Operators
f $ g $ h $ 10
=
-- Backtick infix functions
f `apply` g `apply` h `apply` 10
=
-- Math-y infix functions
f apply g apply h apply 10
=
-- Dot functions; not very applicable here
f.apply(g.apply(h(10))
=
-- Normal functions
f(g(h(10)))
let f = g . h
=
let f = g `compose` h
=
let f = g compose h
=
let f = g.compose(h)
=
let f a = g(h(a))
let newList = n `insert` oldList
=
let newList = n insert oldList
=
let newList = oldList.insert(n)
=
let newList = insert(n, oldList) What about functions of >2 arguments, though? It's actually doable with auto-currying which is a topic that has been discussed quite a bit for Rust. This is specifically useful when you're doing higher-order programming, and sending functions around to other functions, or returning functions; like let insertValue = (`mapInsert` value)
let addPair = insertValue(key)
addPair(hashMap)
=
(key `mapInsert` value)(hashMap)
=
mapInsert(key, value, hashMap) We also have to think about the default associativity and precedence of infix functions. In Haskell they are left associative and have the strongest precedence so they bind tightly. Maybe it would be nicer if they bind weakly instead? And should it be user definable? Assuming it is, how would that be made verbose to the user? It wouldn't be easy to figure out without attaching it to the name of the function itself as far as I can think, so infix functions with different associativity and precedence can be a bit opaque to users, which is alike the argument against user-definable operators. -- Tight precedence
(10*n+1) `insert` myList
-- Weak precedence
10*n+1 `insert` myList These are tradeoffs in tightly binding vs weakly binding by default. |
Yes, this seems to be the central problem... It is solvable, but one has to decide which way to go...
This sounds like an awesome idea! It could just compile to
The problem with procedural macros here is that you have to use the procedural macro, inducing the visual overhead of the macro itself. Having to do:
One has to remember that unlike rust, haskell is a lazy garbage collected language. Rust is both eager and has manual memory management. Thus, every time you partially apply a function, its arguments must be moved to the heap, and then you have to deal with |
I think by now, enough has been fleshed out to create an RFC out of this issue... what does @bluss think? There is one area in particular where some help is needed...
|
Lambdas are not automatically/implicitly moved to the heap, only if you explicitly box them (and with |
(Maybe this discussion is getting off-topic?)
True, my bad. However, if you return the function itself, then that function's arguments must be boxed & It follows that if you want to pass the lambda to another function as an argument that the same logic applies. In most cases (this is just a hypothesis) where you'd want to partially apply a function, you either want to return the function or pass it to something else. Purely partially applying a function and using it within the same stack frame doesn't seem like a big use case to me. |
I like the IMHO this syntax should only support binary functions. I would also recommend against any precedence or associativity declarations, like
That would be a nice touch. |
Methods are functions, so |
I want to argue against this or anything even vaguely like this, on legibility grounds. People are going to write
and nobody, including the original author six months later, is going to have any idea what it means. As a rule of thumb, a mixed chain of infix operations is illegible unless there is an operator precedence rule that everyone not only agrees on, but has had drummed into their head since elementary school (i.e. Declaring that "all such operators have the same precedence and are universally (right|left) associative" does not help, because that rule hasn't been drummed into everyone's head since elementary school, so reading it that way is not automatic. |
I thought about this some more, and my concerns can be addressed with a little syntactic salt and a function annotation. There are two legibility problems with user-defined operators. One is that the precedence of user-defined infix operators, relative to any other operators (user-defined or not), is unclear: what does Now, mathematicians make up operators all the time — how is this not a problem for them? They always parenthesize, unless both precedence and associativity are unambiguous. They have a fairly broad idea of "unambiguous precedence"; I suspect a mathematician would say that of course dot product has higher precedence than scalar addition. However, their idea of "unambiguous associativity" is quite narrow: if a mathematician writes So I propose the following rules:
|
@zackw Thank you for a very well thought-out and reasoned reply! |
Bikeshedding: a \dot b vs. a `dot` b vs. other syntax ? |
Just my opinion: backticks should be reserved for some future hypothetical kind of string literal. |
@zackw seams like a reasonable objective reason to prefer:
I like that your reasoning is not just based on subjective taste =) |
@ubsan You could 😆, but you usually don't :) I would consider that bad Haskell style. |
@Centril tell that to the wonderful people behind lenses :3 |
@Centril: In (.~) :: Lens s t a b -> b -> s -> t
(%~) :: Lens s t a b -> (a -> b) -> s -> t
(+~) :: Num a => Lens s t a a -> a -> s -> t The common idiom is to use them like
The operators are binary operators that return functions, and You could also use them in a fashion like
to build a pipeline of mutations, but this is a less common form. As far as Rust is concerned, I have no dog in this fight, but in Haskell it works quite well and I wouldn't consider it unidiomatic to pipeline functions together in a functional language. ;) |
Nor do I, it's not about the ability to build function pipelines. My arguments were about the unnecessary nature of infix notation, since one can already achieve piping with existing language features, namely method call syntax. Also, Rust is still a different language, and it's not an automatic positive or an obvious gain to make it more like Haskell. Haskell's got many things right, but its syntax is certainly not a part I would praise. Most of the good stuff lies in the semantics of the language, and Rust has borrowed many parts of it already, notably type classes. For which I'm glad — but I'm also glad Rust's designers didn't copy over the weird syntax along with it. |
Pipe-lining with method call syntax fails in the face of free functions and borrowing.
But understand that this is purely subjective. I happen to love Haskell's syntax. There are no objective reasons for why Rust's syntax is better or why Haskell's is better. It is all about what you are used to. Rust's syntax is as it is and not like OCaml's since it was purposefully designed in such a way to not waste the complexity budget on unfamiliar (to C++ programmers) lexical syntax. |
One feature I love from OCaml (and Haskell too, if I'm not mistaken?) is that you have a set of characters to build names with, including function names, which is the usual letters-numbers-underscore plus the single quote, and another set of characters to build operator symbols with, which is a subset of the ASCII symbol characters. So when you see Maybe we could solve the issue like this and make math library authors happy as well? I still think custom operators should have a fixed precedence level and no associativity (= forced parentheses.) Incidentally, I happen to love OCaml syntax, except for a few weird points. To me it's a good middle ground between Haskell and C-like languages. (Yes, I know OCaml came before Haskell, what I mean is that to me the latter is weirder, not easier.) |
Haskell came before OCaml :)
|
This seems to be a common misconception among programmers; unfortunately, it's false. There are objective reasons, especially when it comes to robustness and fault tolerance, for why one kind of syntax is superior to another. For example, whitespace sensitivity, and in particular, indentation-based blocks are dangerous since transmission through a channel that doesn't respect whitespace can significantly change the meaning of a program. So, no, syntactic choices are not "purely" subjective. And even if they were: why do you feel your "I like Haskell's syntax" argument is stronger than my "I don't like Haskell's syntax" viewpoint? |
Before things get more fiery, I think y'all are arguing the same point -- to quote H2CO3:
I don't think anyone is saying their preference matters more, just that there are views on both sides (and both views are partially, if not purely, subjective). |
Are there any scientific studies to this effect?
Personally, I think it is dangerous to rely on lexical syntax for fault tolerance and robustness; I would much rather as much as possible rely on a type system that makes fragile things not well typed. From my time as a teaching assistant for a beginners course in OOP (Java), I think it is equally easy to misplace braces and parenthesis. My view is that we really use layout syntax even with braces (based on how we format with rustfmt), and that they mostly are redundant noise in the way of reading, but I understand that this is my subjective preference and not a universal constant.
I did not make this claim :) |
Please keep off-topic comments about Haskell's syntax out of this Rust thread :) |
I'm not a Haskeller, but, it is difficult for me to understand why you would consider the comments about another language's syntax, that clearly falls into the category of "Prior Art", to be off-topic on the discussion thread for a proposed feature/RFC for Rust. Sounds more like trying to silence someone who you disagree with rather than arguing the merits one way or another. Odd thing indeed. I'm highly offended by the smugness of telling someone they should shut-up because you disagree with what they have to say. |
@gbutler69 Nobody is telling anyone to be quiet, because this is an RFC issue and the "C" stands for "comment". Also, please read @ubsan comment again and you'll see that there is no malice intended by the words she uses. Objectively there's also nothing wrong with wanting to stay on-topic. I gave @ubsan comment a thumbs up because the debate about Haskell syntax is really hard to follow. It'd be helpful if the relevant parts were explained, so that a Haskell noob like me could follow. I'm just seeing a bunch of squiggly infix operators xD |
That's the problem though. As I've said, I just can't understand how they could be considered off-topic. If all you need to do is call something "Off-Topic" without justification to say that it shouldn't be included in the discussion, that is tantamount to just saying, "I disagree, so, shut-up". I don't particularly care for that. If that wasn't the intent, then, my apologies for misreading, but, understand, that is how it comes off to myself and probably many others as well. I'm sure others will perceive it differently though. |
In that case, asking the commenter to clarify is highly desirable and appropriate. Calling it off-topic and saying that the comments don't belong because they are off-topic is not. Again, "Prior-Art" is 100% on-topic in any proposal. I just can't see how it couldn't be. |
I'm suspecting @ubsan was half-joking?; I don't mind their comment at all ❤️
Sure thing! (and some bonus parts) EDIT: I hope this was helpful / understandable; if it wasn't, let me know =) Let's start simple. -- Equivalent (up to memory representation) to `enum List<a> { Nil, Cons(Box<List<a>>), }`.
--
-- List is a type constructor from Type -> Type;
-- List Int is applying 'Int' to 'List' giving you back a type.
-- 'a' is a type variable (generic type parameter)
-- 'Nil :: List a' is a data constructor
-- 'Cons :: a -> List a -> List a' as well.
data List a = Nil | Cons a (List a)
length :: List a -> Int -- A function from List of 'a's to 'Int'
length Nil = 0 -- pattern matching on Nil
length (Cons _ xs) = 1 + length xs -- Pattern matching on Cons.
-- explicit quantification of the type variable 'a'
length :: forall a. List a -> Int
length xs = case xs of -- similar to 'match'
Nil -> 0
Cons _ xs -> 1 + length xs
-- A binary function from 'a' to 'a' to 'a'
-- proviso that 'a' satisfies the 'Num' typeclass (trait);
plusMul2 :: Num a => a -> a -> a
-- This is the same; Haskell functions are curried!
plusMul2 :: Num a => a -> (a -> a)
plusMul2 x y = (x + y) * 2 Rust equivalent of the last one (up to currying, laziness, memory model..): fn plusMul2<A: Add + Mul>(x: A, y: A) -> A {
(x + y) * 2
} With respect to the example lens operators: (+~) :: Num a => Lens s t a a -> a -> s -> t
-- explicitly quantified type variables and explicitly showing the currying:
(+~) :: forall a s t. Num a => Lens s t a a -> (a -> (s -> t)) This declares (the type of) the infix ternary custom operator whatever = (field3 %~ reverse) . (field2 .~ baz) . (field1 .~ bar) This defines a top level function More details: https://hackage.haskell.org/package/lens |
In Kotlin, functions marked with the |
Here are a few arguments for placing this feature in a library rather than in the core language The syntax looks slightly alien compared to the rest of rust and it is not very googleable, but if we force the programmer to decorate each function with I will probably only be using the feature in a few math-heavy functions, so at least for me it doesn't require a lot of work to decorate each function separately. If we end up with several competing macro libraries with varying syntax, then we have a change to pick the best one. |
Certainly no more alien than the operators the language already has?
Haskell uses custom search engines (hoogle) to provide you with much better results than google could ever provide. I believe we could do the same. Decorating functions with a proc macro such as this has the problem that proc macros are a very heavy weight DSL authoring mechanism; it costs a lot to make such a DSL. Meanwhile, custom operators, or functions with infix function notation such as in Haskell are extremely easy to make new ones of. |
Would it be possible to define a macro for defining infix dsls? Something like this could be useful. // In library
pub fn dot( .., ..) -> .. { ..}
pub fn cross( .., ..) -> .. { ..}
define_infix_dsl!(my_linalg, [dot, cross])
// In code
#[dsl(my_linalg)]
fn foo() {..} |
Let's experiment ;) |
@nielsle In a DSL, the infix call style could just be enabled for all methods (with a |
I'm trying to find a language that I can write vector math and graphics stuff in more easily than in C++, while retaining good performance. With respect to the people that have commented here saying infix operators are useless or nearly so, the ability to define and use infix operators of well specified fixity makes quite a significant difference in writing legible math-related code and avoiding mistakes. |
Why not decide associativity by vararg functions (at definition time) and when they don't exist, then assume by default left associativity. Note, there are functions which are neither right nor left associative, e.g. the cartesian product: A*B*C=*(A,B,C) != (A*B)*C /\ *(A,B,C) != A*(B*C) |
I would still love to see something like this happen, but there hasn't been activity in this issue for several years, and RFC issues (as opposed to PRs) aren't really a good forum for this kind of thing. I'm going to close this, but I'd encourage people who are interested in it to collaborate on an MCP for it. |
If you really want to, you can make infixes like these x ^pow^ y;
a *dot* b;
a *cross* (b *cross* c *cross* d); in stable Rust today by abusing operator overloads. Example code: use std::ops::Mul;
#[allow(non_camel_case_types)]
struct dot;
struct DotPartial<const N: usize>([f32; N]);
impl<const N: usize> Mul<dot> for [f32; N] {
type Output = DotPartial<N>;
fn mul(self, _rhs: dot) -> Self::Output {
DotPartial(self)
}
}
impl<const N: usize> Mul<[f32; N]> for DotPartial<N> {
type Output = f32;
fn mul(self, rhs: [f32; N]) -> Self::Output {
self.0.iter().zip(rhs.iter()).map(|(a, b)| a * b).sum()
}
} |
Wow, this is super creative and in my opinion looks pretty good too! While at some point in the future it would be nice to have a prettier way to do this, I think most of the use cases that custom infix functions would be good for can make use of this today (such as the dot product, as you demonstrated!) Since it looks like pretty much every implementation of this would be really similar, I'm wondering if you could make a declarative macro would would write this out for you. I may try my hand at this later. |
I have implemented the aforementioned macro and published it in a crate here on crates.io. Any infix operator with a static input type is now a one-liner to define! EDIT: Only problem is Rustfmt screws with it and there's no way to disable the operator spacing. |
There doesn't seem to be much support for supporting custom operators (see #818) in Rust. But what about calling normal binary functions as if they were operators, like haskell allows you to?
The syntax would be:
a `dot` b
to convey scalar product of two vectors a and b.
Another option suggested by @bluss:
Or if it possible without parsing conflicts (@thepowersgang: "but probably not dersirable"):
Which is converted to
or, depending on the function:
The text was updated successfully, but these errors were encountered: