- Proposal: HXP-0001
- Author: Dan Korostelev
- Status: implemented in 3.3.0
Provide new toplevel type Any
to be used for containing values of any type (hence the name)
as a safer alternative to Dynamic
.
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 yieldsDynamic
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 whenDynamic
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.
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));
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.
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.
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 ofDynamic
.
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.