One programming article worth reading is definitely What Color is Your Function? by Bob Nystrom. A good old rant about the design of several programming languages, one that separates them into two groups of good languages and bad languages, depending on a presence of a certain feature.
The gist of the article is describing a feature called “colored functions” and then analyzing languages depending on whether there is some mechanism akin to this. The presence of a callback mechanism for asynchronous programming is basically the only application of these colored functions, but I believe pointing out other similar mechanisms is worth doing.
I am going to first come up with my own generalized and equally gramatically-controversial analogy of so-called “flavoured functions”. In any programming language with flavoured functions, several rules are in place:
- There is a set of all flavours and any function can have any subset of flavours.
- Calling a function with a specific set of flavours is only possible from a function with a superset of flavours, i.e. it is only possible to call a function that removes a flavour and not one that adds it to its own set.
- The entry point of the program has all the flavours.
This corresponds to the analogy of the original article: blue functions are flavourless in this world, while red functions have a specific flavour (“red”) to them. Additionally, these points are commonly true for programming languages that have a similar feature:
- There are several useful functions in the standard library that have a specific flavour (or more of them).
- It is actually possible to call a function with a new flavour but such action requires more considerations from the programmer and as such can be design breaking or dangerous in some sense.
Now I am going to disagree with the original article on two points: that Java is flavourless and JavaScript is not.
In my opinion, Java has one feature that is more of a nuisance than a helpful tool: throws. In Java, throws declaration can be present on any method where it specifies that only specific exceptions can be thrown from inside a method. While this may sound useful at first glance, it quickly becomes very limiting. If you call a method throwing a particular exception, you must either handle the exception via catch, or propagate it outwards by declaring your method as throwing.
There is the exactly same issue to this as the original article describes: it doesn’t cope well with function composition. If you want to call a user-provided method from a non-throwing method, it must be also non-throwing. There is nothing like what the original article describes as “polychromaticity” – a method can either throw or it can’t; you cannot have it decide based on the input (in a way that can be statically checked by the compiler). That’s why noexcept in C++ is such a better feature, as it can be conditionally applied based on any expression that can be evaluated at compile-time. A function that takes a noexcept function can be noexcept on its own, while a function that takes a general function cannot (if it calls it at some point).
On the other hand, JavaScript, from a language standpoint, has no flavours (at the time when the original article was written, now there are promises but they are still esentially callback-based syntactic sugar). Callback-based asynchronous mechanism is what programmers have themselves added to the platforms using JavaScript, so in a sense, the flavours are only implicit. My point is that the same can hold for other scripting languages, like Lua, as well, being so similar to JavaScript. The only difference is that Lua has already had one feature removing the need for callbacks: coroutines. Having runtime-based support for fibers (unlike compile-time support in languages like C# or modern C++ and JavaScript), the asynchronicity of a function doesn’t have to be explicitly stated and it only becomes apparent when coroutine.yield gets called, pausing the fiber and returning execution to the code that started it (the scheduler). I feel like this feature was so perfectly designed that many other languages could have been much better with it as well, JavaScript especially.
Now having these two things resolved, what are some other examples of flavours in languages?
- async in C#. An asynchronous function can be called normally to return a task, and you can also technically wait until the task is finished and then resume (by obtaining Result or calling Wait). However, this shall not be normally done, as the task itself could schedule some code to run on the original thread, which would be blocked unexpectedly this way. Polymorphism over async is unfortunately not possible, and so things like standard LINQ work only for normal functions (and a totally new sent of LINQ functions had to be added for asynchronously enumerable collections).
- throws in Java. As already stated, a method can potentially throw only an exception it is declared to throw, and other exceptions must be wrapped or logged somehow. The entry point of a program can essentially throw any exception, and once you become tired of having to handle exceptions that you are sure can never actually be thrown, you just wrap everything in RuntimeException (which is unchecked). Moreover, checked exceptions can be effectively bypassed thanks to type erasure.
- noexcept in C++ is not a flavour, rather an antiflavour – its negation, i.e. “potentially throwing” is a flavour. Since this is C++, calling a throwing function from a non-throwing one is permitted, but hope nothing is ever thrown, or your program is dead!
- const is another antiflavour in C++ (and similarly readonly as proposed for C# 8), on non-static member functions, but only in the context of a single type (whose methods can be considered entry points here). This time, the language actually prevents you from accidentally calling a non-const function, but it can of course be bypassed via const_cast. Static polymorphism here is also possible, as overloads can differ only in a presence of const.
- Generic parameter constraints in C# and Java (in the context of a generic parameter), applicable on both methods and types. Constraining a generic parameter in some way requires the caller to specify the same (or stricter) contraints and at least in C#, there is no mechanism other than reflection that allows you to call such a function (even if you can determine it at runtime that the call is possible).
- Unsafe functions in Rust also resemble flavours, as an unsafe function can only be called from another unsafe function (unlike, say, in C#). In contrast to other cases however, this flavour can be actually easily added mid-function, so it's not such an annoyance.
That being said, I don't think there is something inherently wrong with flavours in languages (unlike the original article), as some of them help against common mistakes or enforce good programming practice. In a sense, a flavour is an assumption about the context of the call, it is a kind of a hidden constraint on something else than an argument. Imagine a program that requires the user to log in before doing something important – here you might want a “secure” flavour for functions to remind you when you call them fron an “unsecure” context.
In a sense, this is similar to a concept I found in an actually great article, Parse, don’t validate. Here, the concept of encoding a constraint on the value of a function argument is upgraded to encoding a constraint on the environment itself. Of course, all these languages I have listed only have a limited set of such constraints (Java comes closest), so we are still left with using parameters.
No comments:
Post a Comment