Consider the three Erlang functions:

add_one(X) -> X + 1. add_1(X) when is_number(X) -> X + 1. plus_1(X) when is_number(X) -> {ok, X + 1};

plus_1(_) -> {error, "not a number"}.

In the second one there is a trivial run-time assertion in the code that causes a crash.

In the third one the run-time assertion is turned into a well behaved error message.

By hoisting gen_server calls into Gleam and generating a hashed nonce from an mixture of module/functions/argument types it would be possible to have runtime interprocess calls that compile to Erlang that looks like this:

gen_server:call(Pid, {<<"abce1234">>, MyTerm}),

The nonce <<"abcd1234"> is the generated nonce hash. If is effectively a pointer to a type specification and by matching on it you are doing a run time type check.

The compiler would generate remote handling code like:

handle_call({<<"abcd1234">>, Term}, _From, S = #state{}) ->

...

{reply, MyReply, NewState};

handle_call({Nonce, Term}, _From, S = #state{}) ->

{reply, {error, {type_error, {got, Nonce}, {expected, <<"abcd1234">>}}};

handle_call(Msg, _From, S n= #state{}) ->

{reply, {error, 'WTActualF'}}.

So the normal side-effect free type check asserts the functions are invoked with the correct types.

This new blended type check asserts that if the remote function has been invoked, it has been invoked with the correct types and also if something has gone wrong the calling module correctly handles the complete error set.

(Bear in mind that the error set the calling code has to handle is a superset of the error types in this example. It has to deal with I tried to call it, and it timed out and I don’t know if it crashed or wasn’t available or there was a partition or what. The compiler needs to generate code on the invocation side as well.)

Normally type signatures are symmetric.

The callee function declares if you call me with arguments of type A, then I will return a value of type B.

The caller function declares I will call you with arguments of type A and I expect you to return a value of type B.

This is no longer the case. The callee function type spec is the same, but the callers type spec is extended I will call you with arguments of type A and my expectation now at write time is that you will return a value of type B but you may also return one of this list of error types at run time.

We have moved from hard guarantees to fuzzy partial ones. You can think of this as happy-path typing.

Defining a language that naturalistically blends types and interprocess calling contracts is a non-trivial business, with some sharp aesthetic choices to make (and kinda out of scope, but interesting).

It is trivial to craft a message call in another BEAM language that invokes the function with incorrect types. This type system is no defence against Byzantine failure with an untrusted, distributed and malicious partner.

But there is a wider set of failure types — let us call them Brexit failures. One party is reliable and trustworthy and the other party is a chaotic, slapdash, arrogant and ignorant, bloated, self-important, pompous klown kar kavalkade of krap. Normal for software development errors then. Happy-path typing will prevent some of these and make diagnosis of the rest simpler and cleaner.

Moving from ‘I am in error and my message causes failure for you’ to ‘I am in error, and the type error is returned to me’

The code fingerprints and error handling routines have been standardised and hoisted from being a normal write-time task of the developer (implement, re-implement, re-re-re-re-implement) into the write-time task of the compiler writer.