Welcome to the MaleficTypes
library! This library provides a Union
class that allows you to create a union type similar to those in JavaScript. A union type can hold a value of either type A
or type B
, but not both simultaneously. This class provides utility functions to manage and interact with the values it holds.
- Introduction
- Union Class Overview
- Creating Union Instances
- Checking the Held Value Type
- Retrieving Values
- Equality and Hashing
- String Representation
- Examples
- The Annotation And Plugin
- License
The MaleficTypes
library provides a Union
class that allows you to create a union type similar to those in JavaScript. A union type can hold a value of either type A
or type B
, but not both simultaneously. This class provides utility functions to manage and interact with the values it holds.
The Union
class is a generic class defined as Union<A, B>
, where A
and B
are the types of values it can hold. It ensures that only one of the types is non-null at any time.
class Union<A, B> internal constructor(
private val first: A? = null,
private val second: B? = null,
) {
init {
require((first == null) xor (second == null)) { "Either must hold exactly one value at a time." }
}
// Additional methods...
}
To create a Union
instance holding a value of type A
, use the ofFirst
function:
val unionA: Union<Int, String> = Union.ofFirst(42)
To create a Union
instance holding a value of type B
, use the ofSecond
function:
val unionB: Union<Int, String> = Union.ofSecond("Hello")
To create a Union
instance from a value of either type A
or B
, use the of
function. This function uses Kotlin's reified type parameters to determine the type of the value at runtime:
val unionA: Union<Int, String> = Union.of(42)
val unionB: Union = Union.of<Int, String>("Hello")
You can check which type of value the Union
holds using the isFirst
and isSecond
methods:
if (unionA.isFirst()) {
println("Union holds a value of type A")
}
if (unionB.isSecond()) {
println("Union holds a value of type B")
}
Additionally, you can use the isType
method to check against a specific type:
if (unionA.isType(Int::class)) {
println("Union holds an Int")
}
To retrieve the value from the Union
, use the getFirst
or getSecond
methods. These methods will throw an IllegalStateException
if the value of the requested type is not present:
val valueA: Int = unionA.getFirst()
val valueB: String = unionB.getSecond()
The Union
class overrides the equals
and hashCode
methods to provide meaningful equality checks and hash codes based on the held value:
val anotherUnionA: Union<Int, String> = Union.ofFirst(42)
println(unionA == anotherUnionA) // true
println(unionA.hashCode()) // Hash code based on the value 42
The Union
class provides a toString
method that indicates which type the Union
holds and its value:
println(unionA.toString()) // Union(first=Integer: 42)
println(unionB.toString()) // Union(second=String: Hello)
Here are some examples demonstrating the basic usage of the Union
class:
fun main() {
val union1: Union<Int, String> = Union.ofFirst(100)
val union2: Union<Int, String> = Union.ofSecond("Kotlin")
if (union1.isFirst()) {
println("Union1 holds an Int: ${union1.getFirst()}")
}
if (union2.isSecond()) {
println("Union2 holds a String: ${union2.getSecond()}")
}
println(union1) // Output: Union(first=Integer: 100)
println(union2) // Output: Union(second=String: Kotlin)
}
You can use Union
types as parameters in functions to handle multiple types flexibly. Here are examples using the of
method directly in function calls:
fun processUnion(union: Union<Int, String>) {
when {
union.isFirst() -> println("Processing Int: ${union.getFirst()}")
union.isSecond() -> println("Processing String: ${union.getSecond()}")
}
}
fun main() {
processUnion(Union.of(10)) // Output: Processing Int: 10
processUnion("Hello".toUnion()) // Output: Processing String: Hello
}
In these examples, Union.of
is used directly within the function call, demonstrating how to create and use Union
instances without storing them in variables first. You can do the same with the .toUnion()
function, converting any type to a union.
The MaleficTypes
library provides an additional tool to simplify the use of Union
types in your functions: the @UnionOverload
annotation. When used with the xyz.malefic.types
plugin, this annotation automatically generates overloaded versions of your functions for all possible combinations of types in your Union
parameters.
This feature is especially useful for library authors who want to expose functions with flexible parameter types without requiring users to interact directly with the Union
API.
In your build.gradle.kts
, apply the com.google.devtools.ksp
and xyz.malefic.types
plugins:
plugins {
id("com.google.devtools.ksp") version "..." //Choose version based on your Kotlin version but make sure to instantiate before the plugin
id("xyz.malefic.types") version "2.1.1" //Automatically applies the xyz.malefic:types library and xyz.malefic:types-processor through ksp
}
Add the @UnionOverload
annotation to your functions that use Union
types as parameters. This will signal the processor to generate overloads for all type combinations.
@UnionOverload
fun processSingle(value: Union<String, Int>): String =
when {
value.isFirst() -> "First: ${value.getFirst()}"
value.isSecond() -> "Second: ${value.getSecond()}"
else -> "Invalid"
}
@UnionOverload
fun processMultiple(
name: String,
value: Union<String, Int>,
scale: Union<Float, Double>,
): String =
"Name: $name, Value: ${if (value.isFirst()) value.getFirst() else value.getSecond()}, Scale: ${if (scale.isFirst()) {
scale.getFirst()
} else {
scale.getSecond()
}}"
The plugin will generate overloads for every combination of types in your Union
parameters. For the processMultiple
example, it would create:
fun processMultiple(name: String, value: String, scale: Float) =
processMultiple(name, Union.ofFirst(value), Union.ofFirst(scale))
fun processMultiple(name: String, value: Int, scale: Double) =
processMultiple(name, Union.ofSecond(value), Union.ofSecond(scale))
fun processMultiple(name: String, value: String, scale: Double) =
processMultiple(name, Union.ofFirst(value), Union.ofSecond(scale))
fun processMultiple(name: String, value: Int, scale: Float) =
processMultiple(name, Union.ofSecond(value), Union.ofFirst(scale))
This allows users to call the function with any of these combinations without explicitly creating Union
instances or even having the library.
To verify the correctness of generated overloads, you can write tests like this:
@Test
fun `test single Union parameter overloads`() {
val result1 = processSingle("Hello") // Calls Union.ofFirst<String, Int>("Hello")
val result2 = processSingle(42) // Calls Union.ofSecond<String, Int>(42)
assertEquals("First: Hello", result1)
assertEquals("Second: 42", result2)
}
@Test
fun `test multiple Union parameter overloads`() {
val result1 = processMultiple("Test", "StringValue", 1.5f) // Union.ofFirst for both
val result2 = processMultiple("Test", 42, 2.0) // Union.ofSecond for both
val result3 = processMultiple("Test", "StringValue", 2.0) // Mixed Union.ofFirst and Union.ofSecond
val result4 = processMultiple("Test", 42, 1.5f) // Mixed Union.ofSecond and Union.ofFirst
assertEquals("Name: Test, Value: StringValue, Scale: 1.5", result1)
assertEquals("Name: Test, Value: 42, Scale: 2.0", result2)
assertEquals("Name: Test, Value: StringValue, Scale: 2.0", result3)
assertEquals("Name: Test, Value: 42, Scale: 1.5", result4)
}
- Simplified Function Calls: Users can call your functions directly with standard types (
String
,Int
, etc.) without interacting withUnion
APIs. - Cleaner Library APIs: Reduces boilerplate code and enhances usability for consumers of your library.
- Flexibility: Supports functions with any number of parameters and mixed
Union
and non-Union
types.
This project is licensed under the terms of the MIT License.