Static vs. Dynamic Typing
Dynamic typing in languages such as Python of JavaScript has clear advantages for developers that would like to reduce the amount of (keyboard-) typing they have to do. On the other hand, only static typing enables the definition of clear semantics together with enforcing that these semantics are respected. Furthermore, static typing enables compiler optimizations that typically go much further than what would be possible with dynamic typing.
Duck Typing
If it walks like a duck and it quacks like a duck, then it must be a duck is the concept behind the type system in popular languages such as Python, JavaScript, Perl, Ruby, etc. It is an important part in the convenience offered by these languages.
However, duck typing is very limited when it comes to verifying more complex aspects such as the guarantees given by a certain type. In Fuzion, the pre- and postconditions that specify when a feature within a type can be called and what this feature does are essential parts of that type. A different type can only be used if that different type gives the same guarantees. It is not sufficient if the different type just happens to define features that happen to have the same name.
Consequently, for safety-critical code, duck typing that does not take the contracts defined for a type into account, is not suitable.
Type Inference
Powerful type inference can bring a lot of the benefits of dynamic typing to systems using static typing. Examples of Type inference in Fuzion are
Field type inference from assigned value
x := f() # (1)
The type of field x
is taken from the static type of the result of a
call to f()
in this example.
Function result type inferred from returned value
y => g(h(),i) # (2)
The result type of routine h
is taken from the static type of the result of a
the expression g(h(),i)
in this example.
Actual generic parameter inferred from actual argument
# create array of length n filled with x a(T type, x T, n i32) array T => result := array T n (fun (_ i32) => x) hundred_ys := a y 100 # = [y,y,y, ... ] (3)
Where T
is inferred from the actual argument y
passed
to a
. This, however, does not work for formal generic arguments that are
not used in the signature as in
stack (T type, int capacity) is push(x T) is ... top T => ... s := stack 100
Type inference could still be possible once a feature that uses the formal generic for its arguments is called, as in
s.push "a string" # (4)
but this seems to go too far
Feature Templates
The declaration of generic features could be cumbersome as in this example
of times2
that requires an argument of a type that provides in infix
*
operator:
times2(T type : numeric T, x T) => x * 2 six := times2 3 tau := times2 3.14 two_i := times2 (complex 0 1)
The problems with this is the use of a generic constraint that adds additional typing. Furthermore, the generic constraint in this case restricts the allowed types more than needed, so we cannot write
byebye := times2 "bye"
It would be convenient to be able to write
times2(x) => x * 2 six := times2 3 # (5) tau := times2 3.14 two_i := times2 (complex 0 1) byebye := times2 "bye"
and to have the language figure out the actual types that are used here.
Templates and modules
The use of templates with argument types inferred from actual arguments is
ok as long as the declaration of times2
and its uses are local to one
another (in the same file or compilation unit), since then the requirements on
the types as given in the implementation are also local to one another.
However, it would not be desirable for a compilation unit (module) to use this kind of type inference for features that are visible to other compilation units since this would mean that a change in the implementation that should not be visible outside of the module as in
times2(x) => x + x
would result in a change in the requirement on the type of the argument passed to this feature. Consequently, features exported from modules should not be allowed to use type inference for their arguments, but instead define proper types.
Templates and implementation
In a statically typed language, we want to avoid the overhead of dynamic typing. This can be achieved for templates by duplicating code for a template for each call site where it is used, or at least once for every type signature it is used with. So a template actually defines a whole set of actual features.
Templates and redefinition
Should it be possible to redefine a template? If this was allowed, then a call site for a template would create not only an instance of the template, but one for every redefinition as well.
I could imagine that there are cases where this is useful, but I am unsure if this justifies the added complexity in the front-end.
Typing in Fuzion
Fuzion uses static typing with heavy use of type inference such as
- Field type inference shown in (1)
- Routine result type inference shown in (2)
- Generic argument inference from actual argument shown in (3)
- Template argument type inference shown in (5) within one module
however, I consider the following as going too far for now:
- Generic argument inference from calls on generic target shown in (4)
- Template argument type inference shown in (5) across modules
- Template redefinition