Argentum optionals and Type Casting

In Argentum typecast operator has syntax: expression ~ ClassOrInterface. It performs two types of type castings.

  • If the class is a base class for the expression, the result of such typecast is guaranteed, and the "~"-operation has a result of type ClassOrInterface
  • In all other cases, the operation performs a quick runtime type check, and the result of this operation will be an optional: ?ClassOrInterface.

Unlike casts in Java and C++, where it is not required to check the typecast result, in Argentum, it is syntactically impossible to access the value wrapped in an optional type without checking. Therefore, Argentum applications are simply unable to crash due to incorrect types:

result = expression() ~ MyClass
     ? _.myClassMethod()
     : handleIfNot();

In this example:

  • it evaluates the expression,
  • casts its result to given class/interface,
  • check for success,
  • calls method (or does anything else)
  • provides a handler action or default result if cast failed
  • and stores the result.

This construct requires no additional language constructs. It creates no additional names in the scope and and no extra block nesting.

Comparison to mainstream languages

Almost all comments on previous posts r/ProgrammingLanguages were like: "it is not new, it is all seen in other language A, B and C". Maybe, let's examine.

Criteria: When we write code that works on millions of desktops around the world, or in car engine microcontrollers, wrist watches, in smart TVs and medical equipment, we must minimize crashes to the very unavoidable situations and handle corner cases at the place where they occur. Even when creating microservices, crash is not a good idea. All engineering grade software must be resilient. That's why all checks must have uniform and lightweight syntax - no matter what we check - business logic conditions, type casts, null pointers or array indexes, and there must be no way around these checks. And if a language allows to crash in checks instead of handling errors, this syntax must be less easy to write and it must be alarming when you read the code - if you want to crash you app at some point, you need to write it explicitly. So:

  • Languages will be assessed on ease of use of their condition-checking syntax.
  • Languages must not allow to leave casts not checked.
  • If languages perform internal checks and crash apps in some ways, this must be more explicit and wordy than resilient syntax.

Let's compare

Java:

Java <14

var temp = expression();
var result = temp instanceof MyClass
    ? ((MyClass)temp).method()
    : handleIfNot();

Verbose, introduces additional temp variable that prolongs object lifetime and clutters the scope.

Java14+

ResultType result;
if (expression() instanceof MyClass temp) {
    result = temp.method();
} else {
    result = handleIfNot();
}

Verbose, requires special constructs limited to if-s. Syntax doesn't guarantee that result is initialized.

Java has no unsafe casts, but doesn't force business logic to check the cast results. If not checked/handled, app crashes with NPE. And crashing syntax is simpler than one with checking, that's why everybody uses it.

var result = ((MyClass)expression()).method()

Why NPE is the app crash? Actually no one knows how to handle NPEs at the place they are captured. That's why most nowadays Java applications are full of Optional.OfNullable; no one wants to deal with nullables anymore.

C++

auto obj = dynamic_cast<MyClass*>(expression);
auto result = obj
    ? obj->method()
     : handleIfNot();

Verbose, requires additional temp variable.

Can cast without result checking. Has unsafe static_cast and very low level reinterpret_cast that everybody actually use, and some even confuse them.

Even with safe dynamic cast, syntax allows to skip result checking:

auto result = dynamic_cast(expression())->method();

Swift :

let result = (expression() as? MyClass)
    ?.method()
    ?? handleIfNot()

Also Swift, but there is a more complex code instead of method call:

let result = (expression() as? MyClass)
    .map { myFunction($0) }
    ?? handleIfNot()

Almost as in Argentum in one case and different heavier syntax in another case.

Can cast without checking:

let result = (expression() as MyClass).method()

GoLang:

result := func() ResultType {
    if obj, ok := expression().(MyClass); ok {
        return obj.method()
    }
    return handleIfNot()
}()

Verbose; either introduces a potentially uninitialized variable or requires a nested function.

Cannot cast without checking, but has a handy syntax to crash your app:

let result = expression().(MyClass).method()

Rust:

let result = if let Some(obj) = expression().downcast_ref::<MyClass>() {
    obj.method()
} else {
   handleIfNot()
};

Doesn't introduce unnecessary nesting and variables but very verbose.

Cannot cast without checking, but as in Java and Go has some handy syntax that crashes your app that is simpler than a resilient one:

let result = expression().downcast_ref::<MyClass>().unwrap().method();

Resume:

All listed languages have irregular syntax constructions for checked type casts; they are more complex than regular if-statements. Most of nowadays languages allow unsafe casts. In all languages unsafe constructs are shorter and easier to write than resilient ones. That's why Argentum cannot borrow any of these approaches.

Argentum approach:

// this variant leaves information of failure in `result` for later handling
result = expression() ~ MyClass ? _.method();

// this variant handles failure in place
result = expression() ~ MyClass ? _.method() : handling();

// this valiant terminates app, explicitly and intentionally.
result = expression() ~ MyClass ? _.method() : sys_terminate();

It uses a uniform lightweight syntax.

It makes resilient code simpler than one that makes application crash.

Leave a Reply

Your email address will not be published. Required fields are marked *