9 ms·
I have. I generally like Kotlin, but even that makes it a little too cumbersome to work with "newtypes" such as a NonEmptyString type. Here's what I do for a No
by ragnese 5y ago
I have. I generally like Kotlin, but even that makes it a little too cumbersome to work with "newtypes" such as a NonEmptyString type. Here's what I do for a NotBlankString in Kotlin 1.5+:
@JvmInline
value class NotBlankString private constructor(private val value: String) : CharSequence {
override val length: Int
get() = value.length
override fun get(index: Int): Char = value[index]
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = value.subSequence(startIndex, endIndex)
override fun toString(): String = value
companion object {
fun of(value: CharSequence): NotBlankString? = if (value.isBlank()) null else NotBlankString(value.toString())
}
}
The problem is that most APIs in Kotlin and Java (including the standard library as well as almost all third party libraries) work specifically with String, which is a final class. So, using my NotBlankString is a pain in the ass because I have to explicitly call toString() for most APIs.
Also, I do implement CharSequence, because String does. But CharSequence is a terrible interface and we should probably just pretend it doesn't exist. I somewhat regret even acknowledging its presence.
One of the limitations of value classes is that they cannot implement an interface by delegation, either. So I have to implement CharSequence by hand, instead of writing `: CharSequence by value`.
If you define a "newtype" in Kotlin by using a value class, don't forget to override toString to call value.toString(). By default, it's going to print like a data class format: "NotBlankString(value=foo)"
But, overall, this is a much better than the situation in many languages. But it's still just awkward enough that I think a lot of people don't bother.
In my opinion, a statically typed language should HIGHLY prioritize the ergonomics of defining and using custom defined types. Ideally, I would be able to declare somehow that NotBlankString can do everything a String can do, and therefore be able to pass my NotBlankString type into any function that asks for a String. It would also be better for ergonomics if I could define my own type refinement, instead of needing to call a constructor- kind of like how Kotlin does "smart casting" with null and sealed types:
val s: String = getSomeString()
if (s is NotBlankString) {
doStuff(s) // takes a NotBlankString
} else {
doOtherStuff()
}
- scns 5y agoThank you very much for the detailed writeup. Several new things to learn for me in there. Not being able to use delegation here is a bummer, would be a great fit. While trying to write up a clever solution using [0] i stumbled over String.isNullOrBlank() [0] https://kotlinlang.org/docs/scope-functions.html https://kotlinlang.org/docs/scope-functions.html (edit) if (s.isNullOrBlank()) { doOtherStuff() } else { doStuff(s) // takes a NotBlankString } If you are sure it is not null you can use String.isBlank()