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:
when compilation points to the wrong area or reason
the order of parameters
the definition of parameters
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:
the filename is
none
, which in a multi-file setup means you have to look where you may have missed an armthe reason elides point No. 1, which is that you forgot an arm somewhere, not that unit
()
isn’t a constructor forlist
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:
forget a
⇒
in a function defition (let foo = (…) ⇒ {… }
), and you’ll get an interesting error that points elsewhereuse a keyword (like
else
) as a parameter name or forget to close the parameter list with a closing parenforget the
=
in a variable declarationlet 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>
here we have a function that takes a string and then a character
and here we have one that takes a character and then a string
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:
String.index string char
String.split_on_char string char
String.index_from string char int
String.split_on_char_from string char int
String.starts_with string string
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
.