Casts: Dynamic / Ref Types
There are basically two kinds of type casts. First, those that change the
static type of an expression or reference type but do not change the value itself
only the way it is handled by the compiler.
(In Java the are implemented by the checkcast
bytecode with
the possibility to throw a runtime exception.)
Second, type conversions such as (float) Math.PI
that may
change the value they operate on (as to a lower precision value in this case).
Casting References
The need to cast references could be considered a fault in the applications design. Take this example (mix between Fuzion and Java):
Person ref is name String => abstract Student(redef name String, id i64) : Person is study is .. Professor(redef name String, employee_id i32) : Person is teach is .. print_with_ids(l Sequence Person) is for p in l do if p instanceof Student say p.name + " id " + ((Student) p).id) if p instanceof Professor say p.name + " employee id " + ((Professor) p).employee_id)
Changing this code is inherently dangerous. Adding a new person kind
Assistant(redef name String, temp_id i32) : Person is work ..
may cause this code to fail since Assistant
is a case not handled
by print_with_ids
and the compiler has no way to detect this problem
The problem here is that code like this mixes two paradigms. An object-oriented approach with a choice type (union type). There are two ways to fix this, either by using choice types properly or by using the object-oriented approach properly.
Avoid casts with Choice Types
Using choice types, we could do this:
Student(redef name String, id i64) : Person is study is .. Professor(redef name String, employee_id i32) : Person is teach is .. Person : choice Student Professor is print_with_ids(l Sequence Person) is for p in l do match p stud Student => say(stud.name + " id " + stud.id), prof Professor => say(prof.name + " employee id " + prof.employee_id),
Adding the Assistant
as above will cause a compile time error
unless this new type is added to the alternatives used in Person
and all the match
statements on Person
.
Avoid casts using object-oriented techniques
An object-oriented solution is straightforward:
Person ref is name String => abstract id_string String => abstract Student(redef name String, id i64) : Person is redef id_string => "id " + id; study is .. Professor(redef name String, employee_id i32) : Person is redef id_string => "employee id " + employee_id; teach is .. print_with_ids(l Sequence Person) is for p in l do say (p.name + " " + p.id_string)
Again, adding an Assistant
would cause a compile time error
unless the id_string
feature is implement properly.
Avoid casts by adding features to library code
Sometimes, desired functionality might be missing in an object-oriented design that was fixed in some library module. Say we have the following library
external_lib is Person ref is name String => abstract Student(redef name String, id i64) : Person is study is .. Professor(redef name String, employee_id i32) : Person is teach is ..
And we want to implement print_with_ids
in a different module without modifying external_lib
.
my_module is external_lib.Person .id_string String => abstract external_lib.Student .id_string => "id " + id external_lib.Professor.id_string => "employee_id "+ employee_id print_with_ids(l Sequence external_lib.Person) is for p in l do say (p.name + " " + p.id_string)
This approach of adding features is somewhat similar to implementing traits in Rust. There will have to be similar restrictions, i.e., the added features cannot be visible to the original library module or to other modules using that library.
Open question: Does it make sense to export added features if the module adding them is a library itself?
Runtime type checks using type variables
The workarounds to avoid type casts presented above have one important limitation. They do not work if the target of a type cast itself is not a concrete type but a type parameter. To illustrate this, I take an example from the paper A Reflection on Types by Simon Peyton Jones, Stephanie Weirich, Richard A. Eisenberg and Dimitrios Vytiniotis:
Say we want to provide an effect ST
that provides a way to create,
mutate and retrieve values of arbitrary types. Internally, the implementation
of ST
would use some abstract map to hold the actual values. What
type should elements in this map have?
Dynamic in Haskell
The solution in Haskell is to store Dynamic
values that consist
of a pair typeRep
and x
where typeRep
represents the type of x
.
For a type-safe way to extract a value of a given type from an instance
of Dynamic
, Haskell defines an operation eqT
to
compare instances of typeRep
and add magic to the type checker such
that it knows that a successful eqT
implies that the types involved
are equal.
instanceof in Java
Java's equivalent to Haskell's Dynamic
is the reference
type Object
in conjunction with instanceof
type checks
and casts.
Dynamic types in Fuzion
Similar to Java, Any
is Fuzions most generic type. So we could
implement a state effect that can create slots to store values of arbitrary
types as follows:
state is # internally, we use some map from key to Any add_to_map(k key, v Any) is ... get_from_map(k key) Any => ... create(T type) is k := create_key set(v T) => add_to_map k v read option T => v := get_from_map k match v a Any => a.cast_to T nil => nil
What we need for this is an intrinsic cast_to
defined
for Any
as follows
Any ref is ... cast_to(T type) option T => intrinsic
Since ref
values carry type information anyways, such an
intrinsic is easy to provide.
Type Casts in Fuzion
Fuzion should encourage the user to avoid type casts. Choice types and adding features to library code provide ways to avoid casts in cases where concrete types are used.
For the case of a cast to an abstract type defined by a type parameter,
a cast_to
intrinsic defined for Any
can provide the
necessary runtime type checks and be used for casts to types specified by type
parameters.
cast_to
could be used to cast Any
to value or to
ref types. In the following example, casts to ref types would succeed if
the runtime type is an heir type, while casts to value types would fail unless
the type is exactly right:
point(x, y i32). point3d(redef x, redef y, z i32) : point x y. test(T type, a Any) => say (a.cast_to T) a Any := point 3 4 test point a # ok test point3d a # nil test (ref point ) a # ok test (ref point3d) a # nil b Any := point3d 3 4 5 test point b # nil -- we cannot make a specific value type more general test point3d b # ok test (ref point) b # ok test (ref point3d) b # ok c Any := "some string" test point c # nil test point3d c # nil test (ref point) c # nil test (ref point3d) c # nil