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

Open
Centril opened this Issue Apr 13, 2016 · 88 comments

Comments

Projects
None yet
@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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor Author

Centril commented Apr 13, 2016

Also discussed in: rust-lang/rust#8824

@Centril

This comment has been minimized.

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

ketsuban commented Apr 13, 2016

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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.

@ubsan

This comment has been minimized.

Copy link
Contributor

ubsan commented Apr 20, 2016

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

This comment has been minimized.

Copy link
Member

brendanzab commented May 27, 2016

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

@alilleybrinker

This comment has been minimized.

Copy link

alilleybrinker commented May 27, 2016

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

@Centril

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

ticki commented May 27, 2016

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

@burdges

This comment has been minimized.

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

This comment has been minimized.

Copy link

alilleybrinker commented Aug 12, 2016

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

This comment has been minimized.

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 label Aug 17, 2016

@Centril

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor Author

Centril commented Jan 24, 2017

Bikeshedding:

a \dot b

vs.

a `dot` b

vs.

other syntax

?

@zackw

This comment has been minimized.

Copy link
Contributor

zackw commented Jan 24, 2017

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

@Centril

This comment has been minimized.

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

@MajorBreakfast

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 2, 2018

@Lokathor I've now checked on how it is on a French° keyboard and on an US keyboard. And they all have different ways for writing accented letters! Interesting! xD

° The German keyboard has those accents only for foreign languages like French

@golddranks

This comment has been minimized.

Copy link

golddranks commented May 2, 2018

On Nordic/Scandinavian keyboards the ` symbol is really hard to come by: shift + acute accent button and after that, a space bar (because it's a dead key).

@MajorBreakfast

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 2, 2018

@golddranks The position of the key (to the left of backspace) and how it operates seems to be exactly the same as on the German keyboard.

@burdges

This comment has been minimized.

Copy link

burdges commented May 2, 2018

Apologies for the derail, but..

How would you do infix operators in a curried ML-style language, without using `? We cannot use \ since that gives lambda expressions, well not without using |..| ... for lambda expressions. If functions required a declaration prefix, like say fn, then you could use bare words but declare them with op, ala

op<const M: usize, const N: usize> (x: Vector<M>) cross (x: Vector<N>) -> Matrix<M,N> = ...

or

op<const M: usize, const N: usize> Vector<M> cross Vector<N> -> Matrix<M,N>
op x cross y = ...

with the second form being an outer match if multiple forms were provided. You could still invoke cross function style with (cross) x y. Except..

I think this bare word makes syntax highlighting not context free, and not necessarily even determined by the file, which complicates editor tooling. Worse, it creates the same complexity for humans.

Worse still, these languages represent type parameters like value parameters, so more realistic syntax becomes:

fn cross (const M: usize) -> (const N: usize) -> (x: Vector M) -> (y: Vector N) -> Matrix M N = ..

with some early parameters being implicit or inferred when used, which matters when you represent traits with modules.

I'm not even sure if Haskell permits expressions as operators, ala x `semigroup_mult context` y, although presumably that's bad style, if permitted.

@MajorBreakfast

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 2, 2018

@burdges

I can't follow your code examples. Is this Haskell?

Quick recap: The syntax proposed here is intended for method calls. See Add trait as an example. It defines the add() method. With this proposal it's possible to write a add b instead of a.add(b). Rust is not like Swift where operators are functions instead of methods.

I think this bare word makes syntax highlighting not context free

I think syntax highlighting can still be done without type information even if there is no marking like \ or `. In my comment here I list several different examples where the infix operator appears. It's possible to detect an infix operator expression through the shape of it and colorize all items accordingly.

@Lokathor

This comment has been minimized.

Copy link

Lokathor commented May 2, 2018

@burdges but... Rust isn't a curried ML language? I'm not sure how any of your statements would apply to Rust. I'm not even sure how most of them would apply to Haskell. In Haskell, you can only make a prefix function into an infix operator if it's a single binding anyway:

> addMul 4 5 6
54

> 5 `addMul 4` 6
<interactive>:6:11: error: parse error on input ‘4’

> let withFour = addMul 4
> 5 `withFour` 6
54
@ubsan

This comment has been minimized.

Copy link
Contributor

ubsan commented May 2, 2018

@Lokathor fun thing - because haskell is a curried language, you could do

(4 `addMul` 5) 6

also, I'd love to have this feature no matter what it looks like.

@Centril

This comment has been minimized.

Copy link
Contributor Author

Centril commented May 2, 2018

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

@ubsan

This comment has been minimized.

Copy link
Contributor

ubsan commented May 3, 2018

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

@Centril

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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.

@ubsan

This comment has been minimized.

Copy link
Contributor

ubsan commented May 6, 2018

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

@gbutler69

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 6, 2018

@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

This comment has been minimized.

Copy link

gbutler69 commented May 6, 2018

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

This comment has been minimized.

Copy link

gbutler69 commented May 6, 2018

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 7, 2018

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

MajorBreakfast commented May 8, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.