4 ms·
Correct. But, the reason I'm bitching about it is because, in most languages, you then cannot use your Username type in place of a "native" string, where some
by ragnese 5y ago
Correct.
But, the reason I'm bitching about it is because, in most languages, you then cannot use your Username type in place of a "native" string, where some third-party or standard library function just expects a string. So now you have to convert back and forth.
And if this is a language like pre-record Java, fucking forget it. Define the class, implement equals(), implement hashCode(), write a getter for the wrapped string so that you can pass its guts to functions that expect strings, write the constructor. Speaking of the constructor, do you let the constructor throw an exception? Do you make the constructor private and write a factory function? Does that factory throw an exception or return a failure value? Checked exception or unchecked exception?
Now do that everywhere that you want a "newtype".
Is it possible? Absolutely. I've done it. Is the amount of effort for such a simple concept reasonable? No.
And my other point is that I actually want a non-empty string MUCH more often than I want a potentially-empty string.
I think that programmers are especially prone to just internalizing bullshit and papercuts. We like "solving puzzles" and we're pretty smart and adaptable. So when we encounter something that's actually kind of insane, but we eventually figure out a workaround, we completely forget that it was ever insane in the first place (see: Gang of Four patterns).
- scns 5y agoHave you worked with Kotlin yet? Since 1.5 Result<T> is a valid return type. Inline classes are thin wrappers and compiled out. Data classes automatically give you a toString method. With sealed classes you can implement ADTs.
- ragnese 5y agoI 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()