Discussion about infix requirements

I would like to start a discussion about real world use of custom infix operators and how we might be able to find a compromise to add them to ReScript syntax. I’ve mentioned my position a few times, both here and on the ReasonML forum. My understanding is the ReScript team seems open to at least considering infix use cases so let’s chat.

Some background: in the project I’m working on we have created a little standard library. Most of it extends Belt modules but also pieces of Js, for example we prefer Js.String2 over Belt.String and we also have an extension to Js.Dict. It includes bs-webapi so our standard open statements are all under a single namespace.

Within this standard library we have a total of 17 infix operators defined. I ran a quick search for invocations and came up with many hundreds of results across the project.

10 are repetitions of a pair of very useful functions, >>= and >|=. You may recognise >>= as monadic bind, it’s known as flatMap in some circles including Belt (quite literally we have let (>>=) = Belt.Option.flatMap). On the other hand >|= is non-standard, we use it for map. Composing map and bind together is quite common in certain programming styles so we thought similar syntax would help. We have defined this pair for Option, List, Array, and a couple of custom async data structures.

After some discussion internally we are quite happy to lose this pair once letop is available. For example here’s some actual code we implemented using bs-webapi:

let getDomSelectionFromElement = elm =>
  Tiny.Dom.(
    Element.ownerDocument(elm)
    |> Document.asHtmlDocument
    >>= HtmlDocument.defaultView
    >|= Window.getSelection
  );

In my experimental branch using reason-repacked this has become:

let getDomSelectionFromElement = elm => {
  open Tiny.Dom;
  let.bind doc = Element.ownerDocument(elm)->Document.asHtmlDocument;
  let.map view = doc->HtmlDocument.defaultView;
  view->Window.getSelection;
};

The team is happy with this; arguably it’s a huge win and much clearer. But I’m not here to talk about letop :joy: the point is we can discard these monadic infix operators from the discussion.

Of the remaining 7 infix functions, three are general purpose composition operators:

  • let (>>>): (t('a, 'b), t('b, 'c)) => t('a, 'c);
    Composition operator in a testing framework
  • let (<|): ('a => 'b, 'c => 'a, 'c) => 'b;
    Reverse pipe, aka function compose (f <| g becomes (x) => f(g(x)))
  • let (>=>): ('a => option('b), 'b => option('c), 'a) => option('c);
    Composing >>= on optional operations

It would suck to lose these but we’d manage. The testing framework has already been identified as difficult to use; category composition makes it easier, but we’d rather solve the larger framework issue than hang on to the infix operator. Reverse pipe and the Kleisli arrow have mostly been used in tests and experimental code so we can survive without them.

That leaves the 4 operators we really care about; they’re all part of our Option module and modelled after JavaScript operators to encourage the use of option types.

  • let (<??>): (option('a), Lazy.t('a)) => 'a;
    This is modelled on JavaScript’s nullish operator. Technically ?? is a valid infix function in OCaml, but the associativity renders it useless. We made this lazy to more correctly model ?? behaviour (since it compiles to a function call). Thunked fallback values is a pattern we use a lot in TinyMCE for operations that are expensive or have side effects.

  • let (<?!>): (option('a), 'a) => 'a;
    The same as <??> but takes a strict argument. This one is easy to explain; it’s let (<?!>) = Belt.Option.getWithDefault. I have another bs-webapi example to show how this is used:
    node |> Element.ofNode >|= Element.textContent <?!> ""
    In practice we’ve found we work with plain values often enough that this is generally more useful than <??> and we might later swap which one is lazy.

  • let (<||>): (option('a), option('a)) => option('a);
    Here we apply normal logical || to optional values. Examples:
    Some(5) <||> Some(6) == Some(5)
    None <||> Some(6) == Some(6)

  • let (<|>): (option('a), Lazy.t(option('a))) => option('a);
    Depending on your preference this is either the lazy variant of <||>, or just a familiar monad alternative operator with a lazy second argument. This is extremely useful in DOM navigation, where we have a series of directions to take and we want to use the first one that matches:
    firstChild(node) <|> (lazy(nextSibling(node))) <|> (lazy(parentNextSibling(node, isRoot)))
    Someone on the OCaml forum pointed out doing this generates a chain of lazy thunks, where a letop let|| style would not. So perhaps this one doesn’t count since we’re banking on letop in the future; my experimental branch hasn’t progressed that far.

Fun side note: all four of these are implemented as a single switch statement and would be fantastic cases for applying @inline across module boundaries to produce minimal JS. That might also remove the need for lazy variants (since switch statements are lazily evaluated).

So all that leads into my proposal; allow custom infix operators to be defined only when surrounded by specific characters. We have chosen <> brackets but if that conflicts with existing ReScript syntax we could switch to something else.

Thank you for reading my wall of text!

5 Likes

I also experimented with custom infix operators in a pretty sizable app. But I found that even for myself it became confusing. Eventually I switched back to using just functions and pipe-first. For instance the <??> operator, isn’t this much more readable and explicit?:

value->Option.getWithLazyDefault(lazyDefault)

Do you perhaps have some example code that uses these operators, so we can compare the version with operators with another approach to see why these operators make sense?

2 Likes

It’s absolutely personal preference. I’m working in a team where compositional style, and short syntax using infix functions to compose things together, is preferred. We would prefer value <??> lazyDefault.

I only have a couple of examples of <??> in this project, and neither are really good for posting here, which is why I linked to cases where we’ve used similar concepts in TypeScript. The library will be the foundation for future projects so I expect it will see more use eventually. <?!> is used far more often (and perhaps I could post more of those, since it might be renamed to <??>).

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