5 ms·
>how often have you ever written a function that requested a string as input and actually wanted an empty string? To be fair, I do it quite often. Most of the
by ComodoHacker 5y ago
>how often have you ever written a function that requested a string as input and actually wanted an empty string?
To be fair, I do it quite often. Most of the strings I deal with in my code are coming from user input, and most of them are optional. They are usually just passed to/from a database. If the string has some internal meaning (like ULSs or file paths), it usually gets wrapped in an object anyway.
If you're processing some formal language or network protocol, that's another story.
- ragnese 5y agoLet me ask you this, though. If your strings that come from user inputs are optional, doesn't that mean they could also just not be present (as in null)? Why do you need or want two different ways to express "nothing"? Are all of the text fields just funneled right into the database without checking/validating any of them? I've written a number of RESTy/CRUDy APIs and I can't count the number of "check that username isn't empty" checks I've written over the years.
- feoren 5y agoThe argument for disallowing nulls is much stronger than the argument for demanding a compiler-enforced non-empty string. I definitely support the ability to declare variables, including strings, as non-nullable. An empty string is simply analogous to the number 0. It doesn't really overlap in meaning with null. It's true it would be useful to occasionally disallow the number 0, but only very occasionally. The obvious example is division, but having a representation of +/- infinity alleviates some cases. > I've written a number of RESTy/CRUDy APIs and I can't count the number of "check that username isn't empty" checks I've written over the years. Paraphrasing: "I've written the same kind of method over and over throughout my career and have been unable to (or made no attempt to) abstract it away." I love strong type systems, but it doesn't sound like the type system is your problem here. The problem is that you're constantly re-implementing the same business logic.
- ragnese 5y ago> The argument for disallowing nulls is much stronger than the argument for demanding a compiler-enforced non-empty string. I definitely support the ability to declare variables, including strings, as non-nullable. An empty string is simply analogous to the number 0. It doesn't really overlap in meaning with null. It's true it would be useful to occasionally disallow the number 0, but only very occasionally. The obvious example is division, but having a representation of +/- infinity alleviates some cases. Well, of course you should be able to declare something non-null. What do I look like, someone who likes Java? :p My point wasn't that I WANT to use null instead of an empty collection/string, it was that our languages give us multiple mechanisms by which we can pass in "nothing" for strings/collections, but they give us zero ways to ask for a non-empty string/collection. That's super frustrating! Yes, of course "null" and "empty set" are technically and semantically different. But they're close enough that you could actually deal with having only non-empty strings + nulls and be able to mostly express what you want. That's not the case if I actually want a non-empty string in many of today's languages. Not if I want it to be usable with other APIs and the standard libraries, that is. > Paraphrasing: "I've written the same kind of method over and over throughout my career and have been unable to (or made no attempt to) abstract it away." I love strong type systems, but it doesn't sound like the type system is your problem here. The problem is that you're constantly re-implementing the same business logic. Eh, no. I haven't worked in the same language or on the same project for my whole career. So, yeah, I've noticed that I'm pretty much always either defining a bunch of boilerplate types up front or I regret not doing it when a 0 hits the database because someone wasn't careful with their math or had an off-by-one error. Maybe I should publish a book a la Gang of Four and call it "Static Type Patterns". ;) So, yeah. Believe it or not, I HAVE implemented stuff like PositiveInt and NonEmptyString a bunch of times in a bunch of languages. And it's better than not having it, but it still sucks because in several of those languages, it means that I have all kinds of noise converting to and from, e.g., the native string type. And that's because most of the above languages have no concept of "newtypes" and have no intention of letting programmers define or refine "primitives". It's not really about "business logic". It's about "I know how to describe the shape of this data, but my statically typed language won't let me."
- onemoresoop 5y agoI hear your dismay but you could easily build your own library of string validations which can extend however you want and can reuse it as much as you need.
- still_grokking 5y ago> I've written a number of RESTy/CRUDy APIs and I can't count the number of "check that username isn't empty" checks I've written over the years. But you do it only once, thereafter you have hopefully some "Username" type of object which is guarantied to contain a valid username.
- ragnese 5y agoCorrect. 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() }