There have been discussions on tuple and union types at various locations and the Kotlin Features Survey #2 also contains these proposals. The purpose of this topic is NOT:
- To continue the discussion on whether they should be added to the language or not. In fact, I’m also not a fan of complex languages which allow several different ways to reach the same goal and since such type operations compete with the traditional class-based type system, I cannot say that it would be good to add them. Nonetheless, I find them compelling and I want to explore their benefits.
- To discuss the exact syntactical realizations.
Instead:
- I want to put them into a bigger picture and evaluate their potential in tandem.
- Collect and summarize simple but compelling use-cases to see what could be achieved.
Please leave your feedback and contribute to the list of examples and use-cases below.
Type operations
The basic and simple idea is to view types as sets of values and to introduce ways of creating new types mimicking basic set operations.
Product
This is the tuple type. Common syntax for the type definition is (String, Int)
. To enhance readability of value usage, one can give names to the factors (name: String, id: Int)
. Classes model products naturally, as in
class Entity(val name: String, val id: Int)
Union
For example String | Int
. For more expresiveness in when
blocks, one could name the summands String as Result | Int as ErrorCode
. With classes, this can be modelled via inheritance as in
sealed interface Outcome {
class Result(val res: String) : Outcome
class ErrorCode(val code: Int) : Outcome
}
The named syntax with as
is also useful to force disjoint unions. For example, String | String
is equivalent to String
whereas String as Left | String as Right
is basically two copies of String
.
val x: String as Left | String as Right = "hello" as Left
val y = when(x) {
is Left -> x + "!"
is Right -> x + "?"
}
assert(y=="hello!")
Intersection
For example MyInterface & OtherInterface
. The real potential only arises in combination with proper subtypes, see below.
Singleton
Defines a type with a single value. Basically, only one such type is necessary, e.g. Unit
. However, naming that value makes such types useful, for example to emulate enumerations with the help of union types (see below). Syntax is debatable, I just choose $Singleton
for now. This is a type having just one value named Singleton
. Class-based equivalent would be object Singleton
.
A little differently, If x
is already some value of a type, then $x
is the type with the only value x
. Example: $"hello"
.
Subtype
Given a type like Int
, we want to define a new type where the set of values is a proper subset, for example all even integers. To define which values are allowed, one specifies a predicate on that type. Syntax is again debatable, but the type of even integers could be written like this Int @ { it%2==0 }
. The compiler could provide smart-casts if possible, povided that the predicate is a compile time function KT-14652.
Examples and use-cases
Natural numbers
Just a theoretical exercise:
typealias N = $Zero | (N)
val three: N = (((Zero)))
Enumerations
typealias MyEnum = $One | $Two | $Three
is an enum with three values which is equivalent to
enum class MyEnum { One, Two, Three }
The following would also be a type with three values, but they are integers:
typealias MyEnum = $1 | $2 | $3
Return types
Anonymous types can be used as return types directly without defining them separately, for example to model multiple returns or different outcomes.
fun parseNextInt(line: String): (value: Int, nextPosition: Int)
fun String.parseData(): Data | Failure
Parameters
Union types as input parameters are an alternative to overloading. Subtypes are good to model requirements on inputs. For example:
// usually we do this
fun calculate(num: Int): Double {
require(num >= 1)
return sqrt(num - 1.0)
}
// maybe this is better
fun calculate(num: Int@{it>=1}): Double {
return sqrt(num - 1.0)
}
Combination of subtypes
Use union and intersection types on subtypes to create new subtypes.
typealias ShortString = String @ { it.length <= 8 }
typealias Capitalized = String @ { it.firstOrNull()?.isUpperCase() ?: false }
typealias UserIds = ShortString & Capitalized
typealias AdminIds = $"admin" | String @ { it in ["alice", "bob"] }
typealias AllIds = AdminIds | UserIds