Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.
Generics
Generics let you write more general code that is not tied to specific types and is much more reusable, avoiding copy-pasting in many situations. If you have code that is applicable to more than one data type, you can make that type into a generic parameter and use it to write your logic.
Constraints
Generic type parameters can be further constrained to conform to an interface or derive from a base class. By default <T> is the same as <T: Any?>. This constraint is sometimes called an upper bound.
Given example definitions:
interface Diffable<T, Size> {
val size: Size
fun differenceFrom(other: Diffable<T, Size>): Size
}
@JvmInline
value class Input(val text: String):
Diffable<Input, Double>, Comparable<Input> {
override val size: Double
get() = text.length.toDouble()
override fun differenceFrom(other: Diffable<Input, Double>): Double {
return size - other.size
}
override fun compareTo(other: Input): Int = text.compareTo(other.text)
}
interface Producer<T> {
fun nextValue(): T
fun hasNext(): Boolean
}
class Stack<T>(initialValues: List<T>): Producer<T> {
private val values = initialValues.toMutableList()
fun push(value: T) {
values.add(value)
}
fun pop(): T? = values.removeLastOrNull()
override fun nextValue(): T = values.removeLast()
override fun hasNext(): Boolean = values.isNotEmpty()
}
The constraints can be written and used in the following way. If multiple constraints are needed for the same parameter, where clause is used.
Note: E: Any translates to non-optional type.
class TrendAnalyzer<P: Producer<E>, E>
where E: Comparable<E>,
E: Diffable<E, Double>,
E: Any // Non-optional
{
var increasing: List<Double> = emptyList()
private set
var decreasing: List<Double> = emptyList()
private set
fun analyze(producer: P) {
if (!producer.hasNext()) {
increasing = emptyList()
decreasing = emptyList()
return
}
var lastValue = producer.nextValue()
val increasingResult = mutableListOf<Double>()
val decreasingResult = mutableListOf<Double>()
while (producer.hasNext()) {
val currentValue = producer.nextValue()
val delta = currentValue.differenceFrom(lastValue)
if (currentValue > lastValue) {
increasingResult.add(delta)
} else if (currentValue < lastValue) {
decreasingResult.add(delta)
}
lastValue = currentValue
}
increasing = increasingResult.toList()
decreasing = decreasingResult.toList()
}
}
Usage of a type with generic parameters. If there’s enough information, a generic argument can be inferred and doesn’t need to be specified. Just put _ in its place.
val analyzer = TrendAnalyzer<Stack<Input>, _>()
analyzer.analyze(
Stack(
listOf(Input("infer"), Input("type"), Input("_"))
)
)