Discussion about infix requirements

See also https://github.com/rescript-lang/syntax/issues/102

Also, initially I heard about plans (maybe I heard wrong) that ReScript is planning to provide nullish-coalescing operators specifically for option and result types, at the syntax level. So maybe:

a <??> b

…could desugar to:

switch a {
| Some(a) => a
| None => b
}

…and this would make b automatically lazy because of the way pattern-matching works.

I don’t see an issue for this in the syntax repo though, may be worth filing one.

3 Likes

Oh there is a ticket for it! oops. I haven’t been keeping a close eye on the syntax repo, until recently I was only concerned about whether my team would be able to use ReScript at all.

For now I am more interested in a community discussion about infix than imposing my requirements on anyone (and I expect this forum sees more traffic than that issue tracker). If I can get some consensus that it’s a direction we want to take, then I’ll think about making a ticket or just attach my use case to the existing ticket.

I realised that when I started talking about the @inline ticket. I’m actually on the fence here; if @inline can make function argument evaluation lazy it will change the order of execution depending on the function definition and that’s bad. On the other hand if only syntax can be desugared into making arguments lazy it will avoid that potential mess, but it becomes much harder to define new infix functions.

Ya, I am proceeding on the assumption (maybe wrong) that ReScript team wants to find syntactic solutions, as opposed to library-level solutions, to various data flow issues that are common in the functional programming world today.

  • Function composition: syntactic pipe-first instead of library-level operator pipe-last (done)
  • Optional coalescing: syntactic ?? op instead of library-level <??> (e.g.) op (maybe)
  • Result coalescing: syntactic op instead of library-level op (maybe)
  • Async/await: syntactic transform instead of library-level operators or language-level letops (maybe)
1 Like

I’m worried that removing custom infixes and having these things baked into the language would hinder general use. For example if ReScript implements the alt operator at language level for Option and Result then there are plenty of more scenarios at a library level you would want to use them say for parser combinators, FutureResults etc etc, basically any structure where you have two possible values would benefit from a generic operator alt operator. Another case would be lenses without having infixes for that you wouldn’t get a nice syntax for them it would be very verbose and hard to read. Basically anything that is composable would be better with infixed. Another example would be file paths paths.
let myPath = root() </> dir("a") </> dir("b") </> file("c.txt")
vs
let myPath = root()->join(dir("a"))->join(dir("b"))->join(dir("c"))->join(file("c.txt"))
there is an infinite number of possible structures that would read a lot better if we have custom infixes.

3 Likes

Others useful infix operators are these:

+%, +.% -> addPercentage
-%, -.% -> subPercentage
*%, *.% -> calcPercentage = 300 *% 20 => 60
/%, /.% -> getPercentage = 60 /% 300 => 20

I had use those and improved a lot the readability

1 Like

I don’t get why this would be better or more readable than having it as a function.

I personally find 300->addPercentage(60) much more descriptive :blush:

So I absolutely understand where the team’s decision to remove arbitrary infix operators is coming from. And I think its a good decision for better readable codebases.

4 Likes

Consider if we removed all infixes from:
3 * 4 + 7 - 3
Then that would be:
3->multiply(4)->add(7)->subtract(3)
Any sane person would prefer the former syntax since that is more familiar and easier to read. Having a very condensed syntax might look strange at first but when your brain get used to it it becomes very easy to read. My path example above is an example of that to me the infix version is way easier to read than the prefix version all those extra letters and parentheses makes it harder for me at least.

1 Like

Yeah, but the math infix operators are well-known by everybody, and I agree that the </> operator for paths is rather smart too (not that you couldn’t make do with something like Path.(make([dir("a"), dir("b")], file("c.txt"))). But when you come up with a lot of new infix operators, you basically create a DSL, and DSLs might be good for some specific tasks (like parsing), but otherwise can make you code less readable, because instead of just reading plain-ish English you have to remember what all this notation means.

3 Likes

I think it depends, for me, for example, is way faster understand what is going on with an operator because I can immediate separate it from an alphabetic character and is shorter than the function name, for example:

MyRecordWithVeryDescriptiveName.MyExplicativeField + (MyOtherRecordWithVeryDescriptiveName.MyOtherExplicativeField +% OneMoreRecordWithVeryDescriptiveName.MyExplicativeField)

VS

MyRecordWithVeryDescriptiveName.MyExplicativeField + (MyOtherRecordWithVeryDescriptiveName.MyOtherExplicativeField->addPercentage(OneMoreRecordWithVeryDescriptiveName.MyExplicativeField))

Of course, it depends, if you just define an operator for a function used once, probably function name is more explicative, but I think if you use that operator often it can improve your experience.

Obviously, this is my option based on how I read texts, so it can be different between different people

Yeah, this. If percentage calculations are the bread and butter of your codebase, using custom ops is quite justified. But if you only run across some function once in a few months, an explicit name is more self-documenting.

Using a make function with an list of items doesn’t give you the same flexibility since you might want to use different operators between the values composing in different ways. Yes custom infixes are usually domain specific, if you operate in say a program with lots of records and options all over you probably want lenses to help you and using an infix like <//> as the compose operator for those lenses would make your code much more readable. It takes like a second to look that function up and if the ide shows the type signature of the infix function they you can quickly understand what it’s doing. I don’t understand why people are so against custom infixes they exist in many languages Scala, F#, Haskell, Purescript, OCaml, ReasonML and non fp languages as fixed operator overloading C++, C#. Andy’s idea here is that all custom ones gets wrapped in <> that would make it obvious that it’s a custom infix so much easier to spot.

I’m not interested in a style discussion (although one seems to have happened without me). Some people are comfortable with custom infix, others aren’t, I think that’s fine. But neither side should be forcing their opinions on the other.

The thing I want to discuss is whether my proposal is solid, or are there better ideas we can use to direct the infix discussion.

6 Likes

I think that the concern is that if all infix operators are supported, they might be overused in open source projects, making the source code more obscure and harder to read, making the community less accessible to newcomers. I’m not sure what the best decision is, because in some cases they are very helpful but it’s is a trade-off.

4 Likes

In my opinion, the overuse of infix operators is a bad practice like another, if it is clear for the newcomer that custom operators exist and how to find the definition of it, I think is no problem at all.

In the end, I think you can enforce programming language as far as you want but you can’t avoid that people write bad code.

1 Like

I think one good use case for infix operators is when you’re implementing numbers and strings as opaque types. A contrived example:

type t = { age: int, height: int };

Made more type-safe:

type t = { age: Age.t, height: Height.t };

Where Age.t and Height.t are really just ints. They would redefine (+), (-), etc as Age.(+), Age.(-), and so on.

let ourHeightsCombined = Height.(bill.height + ted.height);

This makes something illogical like bill.height + ted.age impossible to compile.

I mention all of this because I think it fits in perfectly with (what I believe to be) the main goal of ReScript: extreme type safety. Custom infixes are a low-friction way to make primitive types even safer.

And this may just be my opinion, but I don’t see a lot of value in infixes that are mainly (only?) aesthetic like *%. Statements like 300.0 *. 0.20 seem much more readable to me than statements like 300 *% 20. The fact that it’s possible to write code that mixes and matches either style in one expression would make it even more of a headache. But if that works for you, then more power to you.

5 Likes

Exactly. Blocking custom infix won’t stop bad code, it will just annoy those of us in teams where good code is created including infix.

The examples of what is and isn’t good code are entirely subjective. If we want to take on an education burden such that our standard of “good code” is more complex at first glance, that’s on us. But we should have that choice.

3 Likes

I prefer Height.add(bill.height, ted.height), with redefined + it’s not clear at first glance that it’s not an int.

1 Like

Yeah, but try doing something more complicated like Px.((sizeA + sizeB) / 2) (where (/) is (t, int) => t). Px.(add(sizeA, sizeB)->div(2)) is OK, but doesn’t quite read as a forumla.

If infixes have to conform to a certain format, like <*>, then that would help the readability IMO. I personally think that’s a good pattern in general: Height.(bill.height <+> ted.height). It makes it clear that this infix isn’t from Pervasives and that it (presumably) adds two numbers.

7 Likes

Seeing as how unrestricted infixes are not on the roadmap for V10, I’d like to know clearly if the maintainers have any intention of including them. I’ll probably bite the bullet and convert my ReasonML code to much more verbose non-infixed anyways, but it would be nice to know if we are making any progress convincing them that it is a good idea for a “smart” language.

1 Like