reasoning about parameter order

Overview

BLUF: I’ve been using ReasonML for Advent of Code again this year, and whilst I’ve become a bit tired of physics puzzles, it has lead me to something I’ve been thinking about for a while: ergonomics of library & language semantics. Basically three things bother me:

None of these are knocks against ReasonML or OCaml mind you; it’s more things that I’ve noticed. Honestly, if ReasonML were a bit easier to parse and had more backeneds, I’d not even write coastML: it’s incredibly close to my ideal language.

compilation pointers

Quick, what does this mean?

lojikil@raven:~/Code/advent/2022$ dune exec bin/nine/nine.exe data/nine/sample
File "_none_", line 1:
Error: This variant expression is expected to have type (int * int) list
       There is no constructor () within type list

It means that somewhere you forgot either an arm to an if-then-else chain, or you don’t have all cases stubbed out in a switch (which is less likely due to the exhaustiveness checker). This bothers me for two reasons really:

  1. the filename is none, which in a multi-file setup means you have to look where you may have missed an arm

  2. the reason elides point No. 1, which is that you forgot an arm somewhere, not that unit () isn’t a constructor for list

A new person going into this might be quite confused as to what the actual issue of this is…​ and there are a few such examples:

  1. forget a in a function defition (let foo = (…​) ⇒ {…​ }), and you’ll get an interesting error that points elsewhere

  2. use a keyword (like else) as a parameter name or forget to close the parameter list with a closing paren

  3. forget the = in a variable declaration let foo (…​)

Many of these will lead you on a wild goosechase for a missing or stray ;, when in reality you simply missed another symbol elsewhere.

NOTA BENE coastML isn’t yet immune to this, I just was hunting down a mistyped keyword this morning…​

order of parameters

Inconsistencies in parameter order often trip me up; as an example, I use String.split_on_char and String.index quite frequently in advent of code: they help split up strings into tokens or to find the location of where I should substring a string. However, they have opposite parameter orders:

Reason # String.index;
- : (string, char) => int = <fun> 
Reason # String.split_on_char;
- : (char, string) => list(string) = <fun> 

Much of the rest of the String, List, and Array modules have similar inconsistencies: what order they take a function or a base type is something you basically have to memorize; for example, that String.init takes it’s function second and String.map and friends take theirs first.

One other thing I’ve run into, although this is consistent, is that OCaml’s base prefers lengths over ends. For example, String.sub s start len was surprising to me; at first I assumed the second integer was the ending position, and you can imagine how wrong that can be!

definition of parameters

An interesting deviation from the above is String.starts_with, which, surprisingly, isn’t even listed in the module defining string lambdas. So we have various order as noted above, but what about the type of parameter?

Reason # String.starts_with;
- : (~prefix: string, string) => bool = <fun>

So here, we have an optional parameter, without a default value. This is convienent insofar as if we needed to apply many prefixes to one string, we can specialize the String.starts_with the string and then apply many prefixes. However, it completely breaks with even the other ordering issues noted above, by having an optional parameter to a function that arguably shouldn’t have one. As well, it’s one more thing to have to remember when thinking about code.

so what to do?

I’ve been thinking about how to actually fix this for coastML, and I think what we’ll need is some sort of language standard for argument order. It should be something like:

source [needle [needle-index]] [index] [destination [destination-index]] [length]

That would mean that we would roughly have the following for the functions mentioned above:

  1. String.index string char

  2. String.split_on_char string char

  3. String.index_from string char int

  4. String.split_on_char_from string char int

  5. String.starts_with string string

  6. String.concat list(string) string

And so on. This should cover all the various ways in which we need to index core values, up to and including items like String.blit or Array.blit.