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 ints or Strings. 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("")