Update Summary: v0.26.0 to v0.29.0
Three months have passed again since the last blog update, but we’ve been busy making Dyvil more robust and expressive than ever, with many new features and resolved issues in the v0.26.0 and v0.27.1 updates. The following major changes have been implemented, along with minor improvements and bugfixes:
- Type Operators
- Union and Intersection Types
- The
none
Bottom Type - Overhauled Optional Types
- Enhanced Visibility and
explicit
Parameters - Abstract Static Methods
Additionally, a new tool was introduced in v0.26.0 called GenSrc. You can learn more about it here.
Type Operators
Introduced in Dyvil v0.26.0, Type Operators are exactly what they are named after: Operators applied to Types. The best way to show this is an example:
infix operator ~> { 120, right }
type ~> <K, V> = Map<K, V>
These two declarations define two things:
A standard-issue custom operator named ~>
with precedence 120
and right-associativity, and
a generic type-alias with the same name, that simply plugs its type arguments into a Map<...>
.
We can now make use of the latter whenever we want to denote something that has type Map<K, V>
:
let map: int ~> String = [ 1 : "a", 2 : "b" ]
This declaration is semantically equivalent to
let map: Map<int, String> = ...
You can also chain multiple types and operators together to form a new type. This uses the same precedence and associativity rules that are already known from ordinary expressions. In addition to that, you can also use prefix and postfix type operators:
type(int ~> String ~> long) == type(Map<int, Map<String, int>>)
int? // Option Types
int | String // Union Types
int & String // Intersection Types
Union and Intersection Types
Union and Intersection Types are special types that were added in v0.26.0 and rely on the new type operator syntax.
Union Types use the infix operator |
while Intersection Types use &
and have a higher precedence. The following
examples illustrate how they work:
func foo(v: int | String) = ...
foo(1) // ok
foo("ab") // ok
foo(1.5) // not ok - double is not a subtype of int | String
As you can see, the compiler only allows values to be passed to foo
if they are either int
s or String
s. Since
double
is not a subtype of either, the third example gets rejected.
While Union Types represent the set union of two types, the available methods usable with the union type as the receiver
are computed with the intersection. This means only those methods are available on an object of type T|U
which are
available through the common super-types of T
and U
. For int
and String
, the common super types are Object
and
Comparable
. Thus, you can call any method from these two classes (unless you use a cast).
func bar(c: Comparable & Serializeable) = ...
bar("abc") // ok
bar(1) // ok
let c: Comparable = ...
bar(c) // not ok
For an Intersection Type to be considered a super-type of another type, both sides have to be super type. In other words,
you have to pass some object that is both Comparable
and Serializeable
to the foo
function to pass type checking.
With that in mind, Intersection Types can be viewed as the opposite of union types. They represent the set intersection
of two types, and provide the union of available methods. Given an object of type T&U
, you can call methods on it
that are defined in either T
or U
.
The Bottom Type none
Complementing Union and Intersection Types, a new extension to the type system was introduced in v0.28.0 called none
,
the bottom type. Similar to how any
is the super-type of every other type, none
is the sub-type of every type.
An expression of type none
can thus be assigned to any other type. Every throw
statement has this type, and functions
that always throw an exception or error can also use it to indicate this property.
func error() -> none = throw new Error()
var i: int = 10
i = error() // valid
var s: String = error() // valid
Overhauled Optional Types
In version v0.28.0, we made a large set of changes to the Dyvil type system called Optional Types. They provide
a way to specify whether or not null
can be returned or accepted at the type-level. In the below example, the variable
opt
holds a value of type String?
(read Optional String). This means you can assign any expression of type String
,
or null
.
var opt: String? = "abc"
An optional value of type T?
cannot be used like an expression of type T
, i.e. it cannot be assigned to T
.
var str: String = opt // error, because opt could be null
var upper = opt.toUpperCase // error, would cause an NPE at runtime if opt == null
An optional value can be unwrapped to a non-optional one using the !
postfix operator. This has the effect of
performing a null check and causing an NPE if the optional value is null
.
var upper = opt!.toUpperCase // valid
If you know something will rarely actually be null
or don’t care about runtime errors, you can use an Implicitly
Unwrapped Optional Type. It uses the postfix type operator !
and automatically performs calls to the !
unwrap
method.
var opt: String! = null
opt = "abc"
var str: String = opt // valid
var upper = opt.toUpperCase // valid
Enhanced Visibility and explicit
Parameters
Update v0.29.0 introduced two new visibility modifiers, private protected
and package private
. The former is
a more restricted version of protected
that only allows access from sub-classes. The latter is an explicit form
of the default visibility in Java. It was introduced because members now have a default visibility that depends on their
kind. The following table shows the default visibility for different member kinds:
Kind | Default |
---|---|
Header | public |
Type Alias | public |
Class | public |
Class Parameter | protected |
Constructor | public |
Method | public |
Property | public |
Field | protected |
Parameters can now have a new modifier called explicit
. It requires the use of a parameter label at all call sites.
func foo(i: int, explicit j: int = 10)
foo(1) // valid, uses default value
foo(1, 2) // error, j has no explicit label
foo(1, j: 2) // valid, j has explicit label
Abstract Static Methods
A minor change in v0.29.0 was support for static abstract
methods. They were added to complement the Virtual Static
Methods from v0.24.0. An example is shown below:
interface FromString { abstract static func apply(value: String) -> FromString }
case class Name(let value: String) implements FromString
case class Text(let value: String) implements FromString
func empty<T: FromString>() -> T = T() as T
empty<FromString> // AbstractMethodError at runtime
let n = empty<Name> // n: Name = Name("")
let t = empty<Text> // t: Text = Text("")