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 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’slet (<?!>) = Belt.Option.getWithDefault
. I have anotherbs-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 letoplet||
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!