Skip to content

Latest commit

 

History

History
122 lines (85 loc) · 5.26 KB

0001-any.md

File metadata and controls

122 lines (85 loc) · 5.26 KB

Any type

Introduction

Provide new toplevel type Any to be used for containing values of any type (hence the name) as a safer alternative to Dynamic.

Motivation

The current way to pass values of any type is typing them as Dynamic, but it's very error-prone because Dynamic completely subverts type-safety and its behaviour is weird in several ways:

  • Field access to Dynamic values yields Dynamic values as well, which actually makes sense, but also further spreads the untyped behaviour which leads to runtime-specific behaviour, like this famous example:

    var a:Dynamic = [1,2,3];
    a.remove(2); // on e.g. JS we get a run-time error because it doesn't have this field

    Not to mention that accessing inexistant fields is also runtime-specific and moreover there can even be no fields at all, if e.g. Dynamic value is holding a basic type like an integer.

  • Array access to Dynamic values yields monomorph types (Unknown<?>) which is inconsistent with field access, and it also weakens the type-safety of code.

  • Dynamic isn't bound to monomorph types which often leads to surprising behaviour when Dynamic is used as a function return type, for example (taken from tink_core README):

    var x = Reflect.field({ foo: [4] }, 'foo'); // x will stay monomorph even though Reflect.field returns Dynamic
    if (x.length == 1) // x becomes {+ length : Int } and that doesn't support array access
        trace(x[0]); // ERROR: Array access is not allowed on {+ length : Int }

This dynamic behaviour might be handy when dealing with the low-level platform specifics but it's completely unnecessary to bring this dynamicness and weaken type safety in the use case where a simple "value of any type" is needed.

Since it's not explicit and very easy to mis-use, it makes user code much less safe and unintuitive in these cases and it disables a lot of compiler features such as static extensions, abstracts methods, extern inline methods, analyzer optimizations. On a lot of targets, accessing Dynamic fields and operators (such as e.g number comparison) generates hidden runtime overhead which obviously hurts performance.

We are telling people to minimize usage of Dynamic because of these reasons, but on the other hand we don't have a good and type-safe way to express a simple fact that a value can be of any type.

Detailed design

I propose introducing a new type called Any that unifies with any other type in both ways, but doesn't provide any field or array access, doesn't support any operators and is bound to monomorphs like a proper type.

It serves one purpose - to hold values of any type, but to actually use that values, an explicit casting is required. That way the code doesn't suddently become dynamically typed and we keep all the static typing goodness, like advanced type system features and optimizations.

The proposed implementation is quite simple and doesn't require compiler changes:

abstract Any(Dynamic) from Dynamic to Dynamic {}

This type doesn't make any assumptions about what the value actually is and whether it supports fields or any operations - this is up to the user.

Usage:

function setAnyValue(value:Any) {
}

// value of any type works
setAnyValue("someValue");
setAnyValue(42);


function getAnyValue():Any {
    return 42;
}

var value = getAnyValue();

$type(value); // Any, not Unknown<0>

// won't compile: no dynamic field access
// value.charCodeAt(0);

if ((value is String))
    // explicit promotion, type-safe
    trace((value : String).charCodeAt(0));

Impact on existing code

Adding a new top-level type itself shouldn't break anything. If we implement this, we should promote usage of Any instead of Dynamic in places where "any" value is needed, but no dynamic features required.

We have a couple places in the standard library where Any would be a good choice, like haxe.Json.parse and Unserializer.run. Let's change them to return Any, but only when a special -D define is active. This will keep compatibility but also provide an easy way to test and migrate existing code bases to the new concept. Later (in Haxe 4) we could invert the define, or just remove it and use Any by default.

Drawbacks

I don't see any drawbacks: it's a type that solve one exact problem and doesn't introduce any new ones. Its name is short and means exactly the purpose of the type. Its proposed implementation is simple and non-invasive.

Alternatives

I was considering stripping down Dynamic to have the same meaning as the proposed Any, but that has the following disadvantages:

  • It's a huge breaking change.
  • It requires changing the compiler.
  • It complicates low-level target-specific code (e.g. reflection, extern helpers, etc).
  • The name Dynamic is long and suggests some kind of dynamic behaviour, so it's better suited for the current version of Dynamic.

Opening possibilities

If we embrace the Any type and leave Dynamic for the "really dirty low-level stuff", we could not take Dynamic into consideration when designing and implementing new type system features, such as method overloading, which is surely going to make life easier.