While working on this post, I discovered that Swift uses already very similar approach of handling the errors.
The implementation is cheapest I could think of, I am aware of the problems with backend, but enough with excuses — it works, so I will be able to play with it and test if the model is useful or not.
To keep things short let’s start with C# approach which is used in Skila as well:
Errors as exceptions
You access an array with index out of its boundary. You divide by zero. In cases like those you throw an exception to indicate the error in computation.
Errors as values
Your common sense, practice, intuition, tells you that for some computations throwing an exception would be too harsh, so instead you return magic value instead to indicate “not a value” value. Probably the best example is String.indexOf
— for such purpose Skila has type Option<T>
and special value null
(it is not null
you know from C# — it is like Scala none
or null
in Swift or Kotlin).
Ok, that’s it.
Wait, we missed the magic ingredient which makes exceptions fun to work with! Consider accessing the dictionary — to provide similar API as with arrays, when the key is not found the exception is thrown, it happens in C# and Skila as well. But it would be useful to be able to try to fetch the value — in C# you have to add another function to the class (TryGetValue
in this case). In Skila you convert one mode (error as exception) to the other one (error as value):
let dict = {String:Int}();
let val ?Int = dict["hi"] $;
Let’s focus on the second line — when we access the dictionary (it is empty) the exception is thrown from the indexer getter. That exception in Skila is special — it is nearby exception (because it is thrown in nearby, or next if you like, function we just called). Then there is dollar — $
— character which works as an exception trap. Only nearby exceptions can be trapped (the others go through) — and if this happens, they are converted to null
value.
You don’t have to write duplicated code as in C#, you simply convert errors as you like — go from null
to exception, or from exception to null
. Both conversions are expressions and they are pretty accurate:
def thrower() String => throw Exception();
let dict = {String:Int}();
let val ?Int = dict[thrower()] $;
Can you tell what will happen? The program will crash — that is because we trapped exception coming from dictionary indexer, not a thrower.
Why not trap all exceptions? Because I would like to distinguish between three outcomes in total — we looked and we found the key, we looked and we didn’t find the key (i.e. we are sure the key is not there), we crashed while looking (because, say, our comparison function is buggy), so we cannot tell if the key exists or not.
Maybe it is an overkill but after a day of playing with exception trap I felt I miss one more feature — consider reading an ini file and then adding key and value pair to properties dictionary:
properties!add(key,value);
When the key already exists we will get exception with a message and stack trace. However we won’t get plenty of information about the context of the problem, like the filename or line number. Sure, we can debug, but we might add those information right away:
properties!add(key,value) && "File ${filename}, line ${line}.";
The mechanism is old as exceptions — chaining. You can chain an exception or a string (Skila will create basic Exception
for you). Since the new exception is created it is equivalent of explicit throw
, i.e. for the outer function such chained exception will be a nearby exception.
And to just describe all the tools required for the job — there has to be a way to close the distance of the exception, otherwise assert
and fail
would be useless:
def [key K] V
set:
•••
fail("Key not found.");
end
Your code calls dictionary indexer, dictionary indexer calls fail
, fail
calls assert
, assert
throws an exception. So for sure such far placed throw
will not be considered as “nearby” from your code perspective. Unless we tamper with the exception distance counter:
def assert(•••) Void
•••
throw Exception(•••) &&>>;
end
def fail(•••) Void
assert(•••) &&>>;
end
This cryptic code &&>>
tells compiler to pass an exception further. Effectively when you write assert
or fail
the exception which is thrown thinks it originated from where those functions were called.