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

Infix notation for function call / invocation. #1579

Closed
Centril opened this issue Apr 13, 2016 · 94 comments
Closed

Infix notation for function call / invocation. #1579

Centril opened this issue Apr 13, 2016 · 94 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@Centril
Copy link
Contributor

Centril commented Apr 13, 2016

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:

a \dot b

Or if it possible without parsing conflicts (@thepowersgang: "but probably not dersirable"):

a dot b

Which is converted to

a.dot( b )

or, depending on the function:

dot( a, b )
@ticki
Copy link
Contributor

ticki commented Apr 13, 2016

This is a TIMTOWTDI feature with very little (any?) gain. It doesn't read terribly well either.

@Centril
Copy link
Contributor Author

Centril commented Apr 13, 2016

Also discussed in: rust-lang/rust#8824

@Centril
Copy link
Contributor Author

Centril commented Apr 13, 2016

TIMTOWTDI = There's more than one way to do it

There's nothing inherently wrong with having more ways to do one thing.
Some prefer reading a dot b to a.dot( b ) since it gives a more "mathematical" feel to it.

@bluss
Copy link
Member

bluss commented Apr 13, 2016

a \dot b is the nicest syntax I have seen proposed for this (taken from the previous discussion).

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, x.dot(y) is infix. The infix binary function call proposal has advantages:

  • Functions already support: namespacing, importing and renaming. Functions can also use their own traits that they defined for the operands, so we get quite a lot of flexibility by reusing an abstraction we already have (functions are great!).
  • It staves off parenthesis build up. Methods may result in nested calls like x.dot(y.dot(z.dot(w)))).
    • x \dot y \dot z \dot w is easier to read and even a parenthesized infix call version is easier to read: x \dot (y \dot (z \dot w)).
  • Free functions can use a conversion trait bound on both arguments, where methods can not do that for the self argument.

@ketsuban
Copy link
Contributor

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?

@Centril
Copy link
Contributor Author

Centril commented Apr 13, 2016

Discussion on fixity from IRC, for future reference...:

151205            vfs | Centril: are those to have some hard-coded precedence?
151242          bluss | Centril: if you want to. You can just say that either one or the other is a possible
                      | implementation
151301          bluss | proposals like this are judged from superficial issues like syntax too
151321        Centril | vfs: it would have to I guess, how high it should bind I don't know yet
151331          bluss | I mean, the syntax is important in the end, but it doesn't decide if the feature is worth
                      | having or not by itself
151357        Centril | bluss: would it be possible to skip the \ alltogether and just have a dot b ?

                                                            [...]

151924            vfs | Centril: what about associativity? bluss comment seems to suggest right associative, but
                      | afaik all rust operators are all left associative.
151942        Centril | vfs: haskell says:
                      | http://stackoverflow.com/questions/8139066/haskell-infix-function-application-precedence
151956        Centril | infixl 7 `dot`
152204          bluss | vfs: wouldn't the comment suggest left associative, if the (x (y  z)) version is the one
                      | needing explict ()'s
152220        Centril | so in haskell, a `plus` b `plus` c == (a `plus` b) `plus` c
152256            vfs | bluss: `x.dot(y.dot(z.dot(w))))` <-- this does if i read it correctly
152353        Centril | vfs is correct that it is right associative because it evaluates z.dot(w) fist
152357          bluss | anyway, it's not intended to suggest anything
152453        Centril | vfs bluss: one could (possibly) add an attribute to a function specifying its fixity if a
                      | default one isnt good
152533            vfs | what if function is in another crate?
152549       oli_obk_ | Centril: and how would that work together with other infix functions?
152636        Centril | vfs: then the fixity information would have to be carried over into the metadata in some
                      | way i guess
152644          bluss | vfs: the attribute should be on the function
152655          bluss | You'd use this with a function intended to be callable infix
152754        Centril | or, maybe you could know if it is left associative or right depending on where \ is, so x
                      | \dot y or x dot\ y
152807        Centril | maybe thats a bad idea, just brainstorming atm
152828        Centril | oli_obk_: ideas :) ?
152842            vfs | bluss: so you would need to external crate metadata to know how to parse an expression?
                      | (maybe it is the case already now, idk)
152917          bluss | vfs: that's a good point, that doesn't sound workable to me
152919        Centril | guess we need someone who knows the compiler better to flesh that out
152931          bluss | even if crates can inject macros etc
152947        Centril | so maybe it a better idea to specify fixity at call site?
153039       oli_obk_ | Centril: you can't decide it globally. So you can say that a function has a higher fixity
                      | than another function. This way you get a tree of rules depending on the crate dependency
                      | tree. Similar to impl orphans
153117       oli_obk_ | Centril: then you can simply force infix to require brackets if you're specifying it at
                      | the call site
153129       oli_obk_ | and if you are doing that, you can go the macro way
153212       oli_obk_ | a `$lhs:expr $fun:path $rhs:expr` => { $fun($lhs, $rhs) } rule should do it
153255        Centril | so, umh, rephrase .P ?
153351       oli_obk_ | which part? the one about fixity at the call site or the fixity tree?
153423        Centril | all of the above :P
153555       oli_obk_ | if you want to specify the fixity at the call site, you can simply use brackets and force
                      | them. so it would be something like `\(a dot b)` transforms to `dot(a, b)`, but then you
                      | don't gain much over macros: `i!(a dot b)` might be possible, maybe one needs `i!(a \dot
                      | b)`
153722       oli_obk_ | if you want to specify the fixity at the definition site, you need to specify it for all
                      | infix functions that your infix function can be in the same expression with. That might
                      | not be very feasible, but at least complete.
153724        Centril | well, if you have a default fixity, say infixl, then you can write a \dot b \dot c , or
                      | force it: a \dot (b \dot c)
153822        Centril | oli_obk_: haskell does it at definition site, but it has a default fixity of infixl 9
153902       oli_obk_ | that works if you only have one function
153914       oli_obk_ | but what about `a \foo b \bar c` ?
153921        Centril | problems =)
153932        Centril | or: first come first served
154108       oli_obk_ | Centril: what about `vec \add_each_value vec2 \mul_each_value vec3` ? (in matlab terms:   
                      | `vec + vec2 .* vec3`
154206        Centril | well yes, that isnt so nice since that would eval to (vec1 .+ vec2) .* vec3

                                                            [...]

154759        Centril | oli_obk_: but i think that left associative with first-come-first-serve still might be a  
                      | good simplification that makes it crystal clear
154831        Centril | otherwise you have to read about the fixity in the documentation, etc.

@bungcip
Copy link

bungcip commented Apr 13, 2016

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 foo, I expect the function to be used like this: foo(a,b). When I want to create custom infix notation, I expected to implement the necessary trait.

see previous discussion in https://users.rust-lang.org/t/infix-functions/4724

@durka
Copy link
Contributor

durka commented Apr 20, 2016

Does it interact with generics? Would we end up with a \dot::<Foo<'a>, Bar> b?

@Centril
Copy link
Contributor Author

Centril commented Apr 20, 2016

@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 \dot for cases when the types can be inferred from the Exprs.

@strega-nil
Copy link

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.

@brendanzab
Copy link
Member

Infix functions are great in languages like Haskell where operators are more common, but it doesn't seem to give Rust much.

@alilleybrinker
Copy link

Are there examples of projects where this would result in a decent ergonomic improvement?

@Centril
Copy link
Contributor Author

Centril commented May 27, 2016

@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.
Essentially, inside and outside anywhere where a lot of math is needed.

There might be other projects of course, but off of my head, that's the typical use case.

@ticki
Copy link
Contributor

ticki commented May 27, 2016

@Centril The existing overloadable operators covers 95% of those use cases.

@burdges
Copy link

burdges commented May 27, 2016

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 . notation around mathematical objects. If for example, we're doing an action, scalar multiplication, etc. then object notation looks like a left action, which gets annoying if you are not a group theorist from Britain or something.

I fear this proposal makes the left vs right action issue worse. In other words, a \dot b might be more useful for mathematics as b.dot(a) than as a.dot(b), but that seems confusing in non-mathematical situations.

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.

@alilleybrinker
Copy link

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.

@Shou
Copy link

Shou commented Aug 17, 2016

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 . and $ are very convenient control operators that also eliminate the overhead of parentheses, like @bluss said. Respectively they are function composition, and function application. Here are some more concrete usage examples:

-- 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 map. And once you have auto-currying you can also partially apply infix functions.

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.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 17, 2016
@Centril
Copy link
Contributor Author

Centril commented Aug 18, 2016

@burdges: I fear this proposal makes the left vs right action issue worse. In other words, a \dot b might be more useful for mathematics as b.dot(a) than as a.dot(b), but that seems confusing in non-mathematical situations.

Yes, this seems to be the central problem... It is solvable, but one has to decide which way to go...
The various alternatives seem to be:

  • Global fixity, either always left or right associativity. So: a \cross b \cross c == (a \cross b) \cross c or: a \cross b \cross c == a \cross (b \cross c). This is likely the worst possible solution.
  • Definition site precedence and fixity, which how haskell does it. This is problematic, since fixity and precedence must be stored in the crate metadata - and if the library did not store fixity information because the developer omitted it, then you must fall back to a default fixity like haskell does. This also means that this information must be looked up in the documentation, and since we don't have a nice tool like ghci :i to get the fixity this can slow developers down a notch - but I guess this is a problem with function documentation anyways - you still have to read it.
  • Call site fixity: Left and right is decided by where the infix syntax modifier is located, or how it looks like. For example: vec \add vec2 \mul vec3 == vec.add( vec2.mul( vec3 ) ), while vec /add vec2 \mul vec3 == vec3.mul( vec.add( vec2 ) ). /add could be replaced with add\, or if possible, you could use !add and add!. All in all, this seems a bit arbitrary, so I prefer definition site fixity.

@burdges: 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.

This sounds like an awesome idea! It could just compile to HIR directly perhaps and let rustc do the rest...

@AndrewBrinker: 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.

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: infix!(a \cross b \cross c) is not really pretty.

@Shou: 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 map. And once you have auto-currying you can also partially apply infix functions.

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 Box, Rc, Arc and so on...
Some developers might start using currying by default as a nice style of writing, but be completely unaware of the massive relative performance penalty.

@Centril
Copy link
Contributor Author

Centril commented Aug 18, 2016

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...
Regarding storing the fixity and precedence in the crate metadata:

  • does Rust currently store any metadata for anything else?
  • how difficult + wise/unwise would this be?
  • what are the potential problems and pitfalls?

@comex
Copy link

comex commented Aug 18, 2016

Thus, every time you partially apply a function, its arguments must be moved to the heap, and then you have to deal with Box, Rc, Arc and so on...

Some developers might start using currying by default as a nice style of writing, but be completely unaware of the massive relative performance penalty.

Lambdas are not automatically/implicitly moved to the heap, only if you explicitly box them (and with impl Trait it will be possible to avoid that in many more cases than today). Any partial application syntax would presumably desugar the same way.

@Centril
Copy link
Contributor Author

Centril commented Aug 18, 2016

(Maybe this discussion is getting off-topic?)

Lambdas are not automatically/implicitly moved to the heap, only if you explicitly box them, and with impl Trait it will be possible to avoid that in many more cases than today. Any partial application syntax would presumably desugar the same way.

True, my bad. However, if you return the function itself, then that function's arguments must be boxed & move:ed, https://doc.rust-lang.org/book/closures.html#returning-closures

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.

@tobia
Copy link

tobia commented Jan 15, 2017

I like the \dot syntax, especially for mathematical libraries, but I would caution against going too far with it. Porting ($) and (.) and half of Haskell syntax and semantics into Rust, as @Shou seems to suggest, seems like a horrible idea.

IMHO this syntax should only support binary functions. I would also recommend against any precedence or associativity declarations, like infixr 9. The reason is that operator precedence bugs are already common enough, no need to introduce additional crate-defined rules into the mix. These "synthesized" operators should all have the same precedence and associativity. I'm personally fond of universal right-associativity, from APL, but left-associativity would probably be more obvious to everybody else.

a library-defined operator whose return type is the same as that of the first operand [should] be usable as an augmented assignment. How easy is that to do?

That would be a nice touch. a =\dot b; or a \dot= b; or perhaps a \=dot b;

@bluss
Copy link
Member

bluss commented Jan 15, 2017

Methods are functions, so &mut v \Vec::push 1 would be allowed if the syntax is arg1 \<callable expression of two arguments> arg2.

@zackw
Copy link
Contributor

zackw commented Jan 24, 2017

I want to argue against this or anything even vaguely like this, on legibility grounds. People are going to write

x = a \dot b \wedge c \dot e \wedge f \hat a \dot g

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. a + b * c). We already have enough trouble with C's less obvious operators (do you remember what a | b & c means? are you sure? how about if I throw a shift in there, are you still sure?) -- I was actually a little disappointed to discover that Rust had adopted them verbatim instead of requiring parentheses.

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.

@zackw
Copy link
Contributor

zackw commented Jan 24, 2017

I want to argue against this or anything even vaguely like this, on legibility grounds.

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 a \dot b + c mean? The other is that their associativity is unclear: what does a \dot b \dot c mean?

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 a $ b $ c without parentheses, then there is a theorem or axiom that a $ (b $ c) ≣ (a $ b) $ c ∀ a,b,c, i.e. it doesn't matter whether a $ b or b $ c is evaluated first.

So I propose the following rules:

  • \foo has no precedence. That means, it can appear as the sole operator in a complete arithmetic expression:

      if a \isgreater b { ... }
      x = y \dot z;
    

    but it cannot be combined with any other operator unless parentheses are used to make the precedence clear:

      x = (y \dot z) + c; // ok
      x = y \dot (z + c); // ok
      x = y \dot z + c;    // syntax error
    
  • \foo also can't be combined with itself normally, but you can annotate the definition of foo to declare that it's an associative operation in the mathematical sense, and then it's OK:

      // \cross = 3D vector cross product, not associative
      x = (y \cross z) \cross w; // ok
      x = y \cross (z \cross w); // ok
      x = y \cross z \cross w;    // syntax error
    
      // \mprod = matrix product, _is_ associative
      // #[associative] annotation on the definition of \mprod
      x = y \mprod z \mprod w; // ok
    

@Centril
Copy link
Contributor Author

Centril commented Jan 24, 2017

@zackw Thank you for a very well thought-out and reasoned reply!
I really like your solution and would very much like to write an RFC based on it with your (and anyone elses) input.

@Centril
Copy link
Contributor Author

Centril commented Jan 24, 2017

Bikeshedding:

a \dot b

vs.

a `dot` b

vs.

other syntax

?

@zackw
Copy link
Contributor

zackw commented Jan 24, 2017

Just my opinion: backticks should be reserved for some future hypothetical kind of string literal.

@Centril
Copy link
Contributor Author

Centril commented Jan 24, 2017

@zackw seams like a reasonable objective reason to prefer:

a \dot b

I like that your reasoning is not just based on subjective taste =)

@Centril
Copy link
Contributor Author

Centril commented May 2, 2018

@ubsan You could 😆, but you usually don't :) I would consider that bad Haskell style.

@strega-nil
Copy link

@Centril tell that to the wonderful people behind lenses :3

@Centril
Copy link
Contributor Author

Centril commented May 3, 2018

@ubsan I've never used the lens package that way and I've used it fairly extensively (including the infix operators). I usually first bind x op y to a variable and then I use binding z. Besides, my guess is that most of the lens operators are binary.

Tho, cc @ekmett on lens since they made it ;)

@ekmett
Copy link

ekmett commented May 5, 2018

@Centril: In lens a lot of the operators are technically ternary, but are used in the fashion @ubsan mentions. With slightly lobotomized type signatures:

(.~) :: 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

foo 
  & field1 .~ bar
  & field2 .~ baz
  & field3 %~ reverse

The operators are binary operators that return functions, and & is just x & f = f x.

You could also use them in a fashion like

whatever = (field3 %~ reverse) . (field2 .~ baz) . (field1 .~ bar)

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. ;)

@H2CO3
Copy link

H2CO3 commented May 6, 2018

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.

@Centril
Copy link
Contributor Author

Centril commented May 6, 2018

@H2CO3

My arguments were about the unnecessary nature of infix notation, since one can already achieve piping with existing language features, namely method call syntax.

Pipe-lining with method call syntax fails in the face of free functions and borrowing.

Haskell's got many things right, but its syntax is certainly not a part I would praise.

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.

@tobia
Copy link

tobia commented May 6, 2018

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 foobar you know it's a function or variable name, but when you see +:* you know it's a custom infix operator. No ambiguity.

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.)

@Centril
Copy link
Contributor Author

Centril commented May 6, 2018

Yes, I know OCaml came before Haskell, what I mean is that to me the latter is weirder, not easier.

Haskell came before OCaml :)

First appeared | 1990; 28 years ago
https://en.wikipedia.org/wiki/Haskell_(programming_language)

First appeared | 1996; 22 years ago
https://en.wikipedia.org/wiki/OCaml

@H2CO3
Copy link

H2CO3 commented May 6, 2018

@Centril

But understand that this is purely subjective.

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?

@Wyverald
Copy link

Wyverald commented May 6, 2018

Before things get more fiery, I think y'all are arguing the same point -- to quote H2CO3:

Also, Rust is still a different language, and it's not an automatic positive or an obvious gain to make it more like Haskell.

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).

@Centril
Copy link
Contributor Author

Centril commented May 6, 2018

@H2CO3

This seems to be a common misconception among programmers; unfortunately, it's false.

Are there any scientific studies to this effect?

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.

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.

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?

I did not make this claim :)
Rust is certainly not going to get Haskell's syntax.

@strega-nil
Copy link

Please keep off-topic comments about Haskell's syntax out of this Rust thread :)

@gbutler69
Copy link

gbutler69 commented May 6, 2018

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.

@MajorBreakfast
Copy link
Contributor

@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

@gbutler69
Copy link

Objectively there's also nothing wrong with wanting to stay on-topic.

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.

@gbutler69
Copy link

It'd be helpful if the relevant parts were explained, so that a Haskell noob like me could follow.

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.

@Centril
Copy link
Contributor Author

Centril commented May 6, 2018

I'm suspecting @ubsan was half-joking?; I don't mind their comment at all ❤️

@MajorBreakfast

It'd be helpful if the relevant parts were explained

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 +~ which takes a value of type Lens s t a a a value of type a and a value of type s and produces a value of type t. The lower case a, s, and t are type variables (parameters). Lens :: Type -> Type -> Type -> Type -> Type is a type constructor taking 4 types as arguments and returning a type. Again, a must satisfy the Num trait / type class.

whatever = (field3 %~ reverse) . (field2 .~ baz) . (field1 .~ bar)

This defines a top level function whatever; The . are function composition.

More details: https://hackage.haskell.org/package/lens
and: https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial

@MajorBreakfast
Copy link
Contributor

In Kotlin, functions marked with the infix keyword can be called with the infix notation: https://kotlinlang.org/docs/reference/functions.html#infix-notation

@nielsle
Copy link

nielsle commented May 8, 2018

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 #[linalg_dsl] or something similar, then the reader has a chance to google for the decorator and discover what is going on.

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.

@Centril
Copy link
Contributor Author

Centril commented May 8, 2018

@nielsle

The syntax looks slightly alien compared to the rest of rust

Certainly no more alien than the operators the language already has?

it is not very googleable,

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.

@nielsle
Copy link

nielsle commented May 8, 2018

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() {..}

@Centril
Copy link
Contributor Author

Centril commented May 8, 2018

Would it be possible to define a macro for defining infix dsls? Something like this could be useful.

Let's experiment ;)

@MajorBreakfast
Copy link
Contributor

@nielsle In a DSL, the infix call style could just be enabled for all methods (with a self param and one other param)

@masaeedu
Copy link

masaeedu commented Apr 19, 2019

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.

@sighoya
Copy link

sighoya commented Oct 12, 2019

@Centril

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)

@joshtriplett
Copy link
Member

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.

@wishawa
Copy link

wishawa commented Jan 20, 2023

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()
    }
}

@PacificBird
Copy link

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.

@PacificBird
Copy link

PacificBird commented Feb 3, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests