A bit of perspective on infix operators and readability :P

Happened upon an article that immediately reminded me of the ongoing discussion on custom infix operators and readability :wink:

For example, while a FORTRAN program would require programmers to know mathematical formula notation, and write commands like:

TOTAL = REAL(NINT(EARN * TAX * 100.0))/100.0

users of COBOL could write the same command as:

MULTIPLY EARNINGS BY TAXRATE GIVING SOCIAL-SECUR ROUNDED.

As you can tell from the COBOL version, but probably not from the FORTRAN version, this line of code is a (simplified) example of how both languages could compute a social security payment and round the total to the penny. Because it was designed not just to be written but also to be read, COBOL would make computerized business processes more legible, both for the original programmers and managers and for those who maintained these systems long afterwards.

Of course I don’t think we should totally give up math notation, or switch to languages like Cobol or Forth, but still I find the example rather thought-provoking.

3 Likes

I really love the use of infix functions in cases like this, where the result is more readable. People have problems with them when a lot of libraries implement their own bespoke infix functions, and when the function names are symbolic rather than alphanumeric. You end up needing everyone to stick to certain conventions.

I really like having infix functions, though. They can make code so much easier to read sometimes. It just takes discipline to avoid going overboard and turning it into code golf.

2 Likes

Not having these is what stops me updating technicalc-calculator to ReScript syntax. There’s two other packages that use this, which could be updated. But I’d rather they all used the same syntax.

If I remember correctly, the core team agreed on adding back infix operators (I’m using them for anuragsoni/routes), is there any rough timeline around this feature?
Maybe you have some idea @Maxim?

Targeting to upstream everything in March.

7 Likes

Your technicalc uses all the infix symbols we already support. Only the declaration site has a slightly uglier syntax:

let \"+" = ...

But the callsites are all still a + b etc.

4 Likes

wat :anguished: I’m a slowpoke, didn’t know that callsite can still use unwrapped operator.

It’s so unfortunate that local opens are not supported yet b/c considering ^^^ it’s the only thing that holds me from converting everything to .res.

5 Likes

Amazing! I’ll give it a shot then!

The callsite can be unwrapped, but only if it’s a supported infix operator (until the general infix feature lands). For example +, *, even the ocaml-specific ones like +.

That’s all I need tbh. I’m not a user of non-standard infixes anyway.

    <div
      className={
        open Cn
        %tw("flex items-center") + className->take
      }>

We felt it’s ugly at first, but got used to it.

1 Like

We felt it’s ugly at first, but got used to it

What does that code do? There’s likely a way simpler way.

It’s so unfortunate that local opens are not supported yet

Does a scoped open like vdanchenkov’s showed not work for your case?

This is the way re-classnames concats css classnames. open technically works but if local opens will be available at some point, I don’t mind to wait. Current react components in.re work so no reason to touch them and introduce odd code that will be obsolete soon and will be a bad example for new developers.

We ended up just adding (+++) which just joins two strings with space if they’re both non-empty. And since +++ lives in a module that is open everywhere, we don’t need local opens.

That said, it’s not a day and night improvement over the bare Cn.make.

To be honest I’ve never fully understood the urge to test on runtime if the CSS strings are non empty, worst case scenario you have an extra space, is there any browser that would trip on that? I’m by no means a CSS specialist so don’t hesitate to prove me wrong ^^

2 Likes

Never understood the benefits of classnames libraries either. Have been happily using string concatenation and string interpolation for that matter ever since and enjoy predictable performance, zero dependencies and a syntax everyone understands (especially our designer).

%tw("flex items-center") + className->take

What does this even do?

%tw produces string and className is of option<string> type. take does this. Though I do ~className="" so it’s of string type within the component.

Why not just do ["flex", "items-center"]->Js.Array2.joinWith(" ")? If you really want to, you can shorten that: cx(["flex", "items-center"]).

May I suggest the example README, converted:

(+) infix operator

Before:

Cn.("one" + "two" + "three")

After:

cx(["one", "two", "three"])

append

Before:

Cn.append("one", "two")

After:

cx(["one", "two"])

fromList

Before:

Cn.fromList(["one", "two", "three"])

After:

cx(["one", "two", "three"])

on

Before:

Cn.("one" + "two"->on(condition))

After:

cx(["one", condition ? "two" : ""])

onSome

Before:

Cn.("one" + "two"->onSome(myOption))

After:

cx(["one", myOption == None ? "" : "two"])

mapSome

Before:

type t =
  | One
  | Two
  | Tree;

Cn.(
  "one"
  + mapSome(
      Some(Two),
      fun
      | One => "one"
      | Two => "two"
      | Tree => "three",
    )
)

After:

type t =
  | One
  | Two
  | Tree

[
  "one",
  switch Some(Two) {
  | Some(One) => "one"
  | Some(Two) => "two"
  | Some(Tree) => "three"
  | None => ""
  }
]->cx

take

Before:

Cn.("one" + Some("two")->take)
Cn.("one" + None->take)

After:

let take = Belt.Option.getWithDefault // or whatever

cx(["one", take(Some("two"), "")])
cx(["one", take(None, "")])

onOk

Before:

Cn.("one" + "two"->onOk(Ok("ok")))
Cn.("one" + "two"->onOk(Error("err")))

After:

open Belt // this works too

cx(["one", Result.isOk(Ok("ok")) ? "two" : ""])
cx(["one", Result.isOk(Error("err")) ? "two" : ""])

mapOk

Before:

type t =
  | One
  | Two
  | Tree;

Cn.(
  "one"
  + mapOk(
      Ok(Two),
      fun
      | One => "one"
      | Two => "two"
      | Tree => "three",
    )
)

After:

type t =
  | One
  | Two
  | Tree

[
  "one", 
  switch Ok(Two) {
  | Ok(One) => "one"
  | Ok(Two) => "two"
  | Ok(Tree) => "three"
  | Error(_) => ""
  }
]->cx

onErr

Before:

Cn.("one" + "two"->onErr(Ok("ok")))
Cn.("one" + "two"->onErr(Error("err")))

After:

open Belt
cx(["one", Result.isError(Ok("ok")) ? "two" : ""])
cx(["one", Result.isError(Error("err")) ? "two" : ""])

mapErr

Before:

// example has a bug

After:

//

none

Before:

Cn.(
  switch (x) {
  | Loading => Css.loading
  | Loaded => ""
  }
);

// vs

Cn.(
  switch (x) {
  | Loading => Css.loading
  | Loaded => none
  }
);

After:

// same

Benefits:

  • No infix addendum, combinator, warning 44, local open, extension point, curry/uncurry.
  • Only api/concepts needed: function, array, switch.
  • Readability++.
  • Juniors and seniors end up writing the same simple code.
  • Check the amazing output difference!
  • Free syntax upgrade =)

I understand the tidiness with the spacing and all, but you can just swap that generic joinWith with another one that collapse spaces if you really want that.

Likewise, you can do:

`one ${condition ? "two" : ""} three`->cleanUp

Equivalent conversion here.

Another perspective is that generating css classnames is the simplest scenario of string concatenation. If we’re resorting to extension point, combinator, infix and others then maybe we need to reconsider.

3 Likes

The main reason of switching to infix api was perf since a + b doesn’t introduce additional allocations. I’m not sure if I checked arrays (should be faster then lists I guess, I will run benchmarks later today) but list api was more than 4 times slower than using infix.

It does introduce lots of intermediate string allocations, though JS fares well with those (at least V8). Theoretically a join should be much faster but in reality I doubt so; JS engines are good but not that good nowadays… so yeah a bajillion string concatenations is likely faster even. The JS output really isn’t great though.

Even if it’s a bit slower, I think the benefit of basically removing the entire cognitive overhead of a task that reduces to concatenating a few strings, is worthwhile and seems right, philosophically speaking.

Also, worst case, you can still use the new interpolation syntax!

2 Likes