Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

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
last changed: 2024-06-28