Skip to content

Types

Types describe the values that a declaration can accept. Safe-DS has various categories of types, which are explained in this document.

Categories of Types

Named Types

Named types either denote that a declaration must be an instance of a class or one of its subclasses, or an instance of a variant of an enum. In either case the syntax of the type is just the name of the class or the enum respectively.

Class Types

A declaration with a class type must be an instance of a class or one of its subclasses. Let us use the following classes for our example:

class SomeClass

class SomeSubclass sub SomeClass

To denote that a declaration accepts instances of SomeClass and its subclass SomeSubclass, we write the name of the class as the type:

SomeClass
Nullable Class Types

The value null (see null) deserves special treatment since it is not possible to operate on it in the same manner as on proper instances of a class. For this reason null cannot be assigned to declarations with class types such as SomeClass.

To specifically allow null as a value, simply add a question mark to the named type:

SomeClass?

Enum Types

A declaration with an enum type must be one of the variants of the enum. Let us use the following enum for our example:

enum SomeEnum {
    SomeEnumVariant
    SomeOtherEnumVariant(count: Int)
}

To denote that a declaration accepts instances of any variant of SomeEnum, use the name of the enum as the type:

SomeEnum

This type expects either the value SomeEnum.SomeEnumVariant (see member access) or anything constructed from the variant SomeOtherEnumVariant such as SomeEnum.SomeOtherEnumVariant(3).

Type Arguments

Note: This is an advanced section. Feel free to skip it initially.

If a declaration has type parameters we need to assign all of them when we use the declaration as a named type. This assignment happens in the form of type arguments. We explain this using the following declaration:

class SomeSpecialList<T>

When we use this class as a named type, we need to specify the value for the type parameter T, which is supposed to denote the type of the elements in the list.Similar to calls, we can either use positional type arguments or named type arguments.

In the case of positional type arguments, they are mapped to type parameters by position, i.e. the first type argument is assigned to the first type parameter, the second type argument is assigned to the second type parameter and so forth.

If a positional type argument is used, we just write down its value, which is a type projection.

For example, if we expect a list of integers, we could use the following type:

SomeSpecialList<Int>

Let us break down the syntax:

  • The usual named type (here SomeSpecialList).
  • Opening angle bracket.
  • A positional type argument (here Int).
  • A closing angle bracket.

When a named type argument is used, we explicitly specify the type parameter that we want to assign. This allows us to specify them in any order. It can also improve the clarity of the code since the meaning of the type argument becomes more apparent. Here is the type for our list of integers when a named argument is used:

SomeSpecialList<T = Int>

These are the syntactic elements:

  • The usual named type (here SomeSpecialList).
  • Opening angle bracket.
  • A named type argument (here T = Int). This in turn consists of
  • The name of the type parameter (here T)
  • An equals sign.
  • The value of the type argument, which is still a type projection.
  • A closing angle bracket.

Within a list of type arguments both positional and named type arguments can be used. However, after the first named type arguments all type arguments must be named.

Let us finally look at how multiple type arguments are passed. For this we use the following declaration:

class SomeSpecialMap<K, V>

This class has to type parameters, namely K and V, which must both be set if we use this class as a named type.

Here is a valid use:

SomeSpecialMap<String, V = Int>

We will again go over the syntax:

  • The usual named type (here SomeSpecialMap).
  • An opening angle bracket.
  • The list of type arguments. Each element is either a positional or a named type argument (see above). Individual elements are separated by commas. A trailing comma is allowed
  • A closing angle bracket.

We will now look at the values that we can pass within type arguments.

Type Projection

The most basic case is that we pass a concrete type as the value. We have already seen this in the example above where we constructed the type for a list of integers:

SomeSpecialList<Int>

The value of the type argument is just another named type (here Int).

Member Types

A member type is essentially the same as a named type with the difference that the declaration we refer to is nested inside classes or enums.

Class Member Types

We begin with nested classes and use these declarations to illustrate the concept:

class SomeOuterClass {
    class SomeInnerClass
}

To specify that a declaration accepts instances of SomeInnerClass or its subclasses, use the following member type:

SomeOuterClass.SomeInnerClass

This has the following syntactic elements:

  • Name of the outer class (here SomeOuterClass).
  • A dot.
  • Name of the inner class (here SomeInnerClass).

Classes can be nested multiple levels deep. In this case, use a member access for each level. Let us use the following declarations to explain this:

class SomeOuterClass {
    class SomeMiddleClass {
        class SomeInnerClass
    }
}

To specify that a declaration accepts instances of SomeInnerClass, or its subclasses, use the following member type:

SomeOuterClass.SomeMiddleClass.SomeInnerClass

If any referenced class has type parameters these must be specified by type arguments. For this we use these declarations:

class SomeOuterClass<A> {
    class SomeInnerClass<B>
}

To specify that a declaration accepts instances of SomeInnerClass where all type parameters are set to Int, or its subclasses, use the following member type:

SomeOuterClass<Int>.SomeInnerClass<Int>

Finally, as with named types, null is not an allowed value by default. To allow it, add a question mark at the end of the member type. This can be used independently of type arguments:

SomeOuterClass<Int>.SomeInnerClass<Int>?

Enum Variant Types

Member types are also used to specify that a declaration is an instance of a single variant of an enum. For this, we use the following declarations:

enum SomeEnum {
    SomeEnumVariant(count: Int),
    SomeOtherEnumVariant
}

To allow only instances of the variant SomeEnumVariant, use the following member type:

SomeEnum.SomeEnumVariant

Let us take apart the syntax:

  • The name of the enum (here SomeEnum).
  • A dot.
  • The name of the enum variant (here SomeEnumVariant).

Identical to class member types, all type parameters of the enum variant must be assigned by type arguments. We use these declarations to explain the concept:

enum SomeEnum {
    SomeEnumVariant<T>(value: T),
    SomeOtherEnumVariant
}

To now allow only instances of the variant SomeEnumVariant with Int values, use the following member type:

SomeEnum.SomeEnumVariant<Int>

Union Types

If a declaration can have one of multiple types you can denote that with a union type:

union<String, Int>

Here is a breakdown of the syntax:

  • The keyword union.
  • An opening angle bracket.
  • A list of types, which are separated by commas. A trailing comma is allowed.
  • A closing angle bracket

Note that it is preferable to write the common superclass if this is equivalent to the union type. For example, Number has the two subclasses Int and Float. Therefore, it is usually better to write Number as the type rather than union<Int, Float>. Use the union type only when you are not able to handle the later addition of further subclasses of Number other than Int or Float.

Callable Types

A callable type denotes that only values that can be called are accepted. This includes:

Additionally, a callable types specifies the names and types of parameters and results. Here is the most basic callable type that expects neither parameters nor results:

() -> ()

Let us break down the syntax:

  • A pair of parentheses, which is the list of expected parameters.
  • An arrow ->.
  • A pair of parentheses, which is the list of expected results.

We can now add some expected parameters:

(a: Int, b: Int) -> ()

These are the syntactic elements:

  • Parameters are written in the first pair of parentheses.
  • For each parameter, specify:
  • Its name.
  • A colon.
  • Its type.
  • Separate parameters by commas. A trailing comma is permitted.

Finally, we can add some expected results:

(a: Int, b: Int) -> (r: Int, s: Int)

The syntax is reminiscent of the notation for parameters:

  • Results are written in the second pair of parentheses.
  • For each result, specify:
  • Its name.
  • A colon.
  • Its type.
  • Separate result by commas. A trailing comma is permitted.

If exactly one result is expected, the surrounding parentheses may be also removed:

(a: Int, b: Int) -> r: Int

Unknown

If the actual type of a declaration is not known, you can denote that with the special type unknown. However, to later use the declaration in any meaningful way, you will have to cast it to another type.

Corresponding Python Code

Note: This section is only relevant if you are interested in the stub language.

Optionally, type hints can be used in Python to denote the type of a declaration. This is generally advisable, since IDEs can use this information to offer additional feature, like improved refactorings. Moreover, static type checker like mypy can detect misuse of an API without running the code. We will now briefly describe how to best use Python's type hints and explain how they relate to Safe-DS types.

First, to get type hints in Python closer to the expected behavior, add the following import to your Python file:

from __future__ import annotations

Also add the following import, which brings the declarations that are used by the type hints into scope. You can remove any declaration you do not need:

from typing import Callable, Optional, Tuple, TypeVar, Union

The following table shows how Safe-DS types can be written as Python type hints:

Safe-DS Type Python Type Hint
Boolean bool
Float float
Int int
String str
SomeClass SomeClass
SomeEnum SomeEnum
SomeClass? Optional[SomeClass]
SomeEnum? Optional[SomeEnum]
SomeSpecialList<Int> SomeSpecialList[int]
SomeOuterClass.SomeInnerClass SomeOuterClass.SomeInnerClass
SomeEnum.SomeEnumVariant SomeEnum.SomeEnumVariant
union<String, Int> Union[str, int]
(a: Int, b: Int) -> r: Int Callable[[int, int], int]
(a: Int, b: Int) -> (r: Int, s: Int) Callable[[int, int], Tuple[int, int]]

Most of these are rather self-explanatory. We will, however, cover the translation of callable types in a little more detail: In Python, the type hint for a callable type has the following general syntax:

Callable[<list of parameter types>, <result type>]

To get the <list of parameter types>, simply

  1. convert the types of the parameters to their Python syntax,
  2. separate them all by commas,
  3. surround them by square brackets.

Getting the <result type> depends on the number of results. If there is only a single result, simply write down its type. If there are multiple types, do this instead:

  1. convert the types of the results to their Python syntax,
  2. separate them all by commas,
  3. add the prefix Tuple[,
  4. add the suffix ].