Hacker News new | past | comments | ask | show | jobs | submit login

It needs to be supported at the type level, whether by null or by options, simply because “data not available” is a common value people need to use. When there’s no good way to express it, they’re forced to invent special sentinel values, and you end up in the situation where array index -1 means “value not found in the array”.



I have worked with a MySQL database where the designer(s) decided that, on many of the tables, -1 should represent no value instead of null. As you can imagine, this has caused some problems when they've done this with columns representing dollar amounts.

This was done because of their belief that having any nulls in the table is the kiss of death for performance; they've used the phrase "tablescan" a lot. I have not been able to find a basis for these claims.


Databases, and MySQL in particular because of how many of it's defaults are pants-on-head nonsensical, are a haven of cargo cult performance rituals. I have a suspicion this is because, rather than analysing their own N! loops/queries, it's easier for mediocre programmers just to blame the database.


Yes, option types are awesome. No, they are not nulls.

Algebraic data types are not direct support "no data found at the type level". Algebraic types are really just a fancier enum/union type. It just so happens that inventing special sentinel values is awesome when you have an algebraic type system to check your work.


Take a look at Kotlin or Typescript†. Basically, they decided to fully design the language with support for null-as-option.

That means several things:

  * T (non-nullable) and T? (nullable) are different types. T? = T | null
  * Where T is expected T? is not accepted, but where T? is specified T is also accepted
  * T? is automatically cast to T in the places where it's asserted to be not null, e.g. within an if(x != null) branch
  * Method calls are not allowed on T? (unless the function specifies it can handle null)
  * There's syntax for providing a value in case of null. (x ?: fallback) in Kotlin, (x || fallback) in Typescript. 
It's a much more pleasant developer experience than the Option ADTs/Enum types from Scala or Haskell, which the same amount of safety.

Typescript goes even further and has the best enumeration support I've seen any language have. T | U is a fully valid type, and if T | U is asserted to be one of them it is automatically cast to T/U. It is a very natural and efficient way of building ADTs [1]

† Typescript 2.0+ with --strictNullCheck on

[1] https://www.typescriptlang.org/docs/handbook/advanced-types.... , Discriminated Unions header


These are great features, but they are really just syntactic sugar over algebraic types. It's not a more pleasant developer experience than sum (Option) types, it's a more pleasant experience with sum types. Ex:

    macro! unwrap(x, fallback) {
      match x {
        Some(n) => n,
        None => fallback
      };
    }
> Typescript goes even further and has the best enumeration support I've seen any language have. T | U is a fully valid type, and if T | U is asserted to be one of them it is automatically cast to T/U. It is a very natural and efficient way of building ADTs

That's pretty cool. It seems like refinement types. [1]

[1]: https://en.wikipedia.org/wiki/Refinement_(computing)#Refinem...


It's subtly different because of the implicit coercions and smart casts. You can always pass T to a function that takes T?, while in Haskell/Rust you would need to pass Some(t). Similarly, Kotlin does control-flow analysis and converts all T?s inside an "if (t != null) ..." block or after an "if (t == null) return" statement into a T, which dramatically reduces the amount of try!/unwrap() calls that I used to see littering early Rust code. There's better syntactic sugar for it in Rust now, but the point is that Kotlin doesn't need nearly as much syntactic sugar because nullable is integrated with the typechecker and flow analysis.


> It's not a more pleasant developer experience than sum (Option) types, it's a more pleasant experience with sum types.

Having worked with both Scala, Haskel, Kotlin and Typescript I disagree, the dev experience for Kotlin/Typescript is miles ahead for optionality. It seems like a small difference vs something like do notation or for comphrehension but it really adds up.

It hard to explain until you've tried it. The most succinct way would be to say that optional code looks and feels nearly identical to non-optional code, instead of unwrap/do-notation which makes a very large syntactical difference.

Take a look at a piece of async Kotlin co-routine code v.s. Java/Scala (Completable)Future, same effect.


If you like those features, I think you would like Crystal, which has much of the same. Full union types, flow typing and type inference for basically everything on the stack makes for a very fluid and scripting language like experience while keeping type safety.


> * There's syntax for providing a value in case of null. (x ?: fallback) in Kotlin, (x || fallback) in Typescript.

Well, uh, unless T can be falsy. 0 || fallback === fallback. A proper ?? operator a la C# would be great, but they've pushed back against it because it doesn't entirely mesh well with nulls in the JS world.

JS is still probably my favorite language to work in despite stuff like this. But that's one footgun that everyone should be aware of.


A true elvis and null-safe call operator like ?., ?? and/or ?: would be an improvement, but the idea doesn't seem to be gaining traction in the Javascript world where || is "good enough"


That's a terrible approach because it's inherently noncompositional and thus breaks parametricity. You can't tell whether T? is the same type as T without knowing what T is, so if you use T? and handle null in your generic functions then they might suddenly misbehave when passed a null.

(Previously said T?? rather than T?, thanks to corrections in replies)


This is incorrect. The type system of both languages makes sure that T does not include null, so the case where you "accidentally" handle the null of T is impossible.

More generally, with unions you always have this behavior but I've never seen this be a problem. If your function accepts T | U and you pass (T | U) | U it simplifies to T | U and in my experience the code that handles U is always the correct thing to do for U.


> The type system of both languages makes sure that T does not include null, so the case where you "accidentally" handle the null of T is impossible.

So does that mean you can't call generic methods with ? types? Because there's an excluded middle here: either something like String? is a first-class type, in which case you can invoke a <T> T ... method with T=String? and then any T?s inside that method have the potential to misbehave, or you can't, in which case String? becomes an awkward second-class type.


What? No. T is a subtype of T?, so I don't get where you are going.

If a generic function f accepts a covariant Dog and Animal is a supertype of Animal you cannot call f with Animal because it requires a Dog. if f accepts a covariant Animal you can call it with a Dog because Dog is a subtype of Animal.

Now replace Dog with T and Animal with T? and you can see it is perfectly fine.


We're talking about generics. <T> and the like. I can have a Map<String, Dog>, I can have a Map<String, Animal>, and these are different things but they both work fine because both Dog and Animal are first-class types.

Can I have a Map<String, Dog?> ? If no, then Dog? isn't a first-class type. If yes, we have all sorts of nasty surprises, because code written in terms of Map<String, T> will assume that if map.get(someKey) is null then that means someKey isn't in the map, and this code will work fine until someone uses a ? type for T and then break horribly.


> Can I have a Map<String, Dog?>

yes

> because code written in terms of Map<String, T> will assume that if map.get(someKey) is null that means someKey isn't in the map

And it can safely assume that. Map<String, T?> isn't a subtype of Map<String, T>, so passing Map<String, T?> to a place requiring Map<String, T> will not compile.

T is a subtype of T?, not the other way around. Map is defined as Map<K, out V>, meaning V is covariant.

So you can pass a Map<String, Dog> where a Map<String, Dog?> is required, but not a Map<String, Dog?> where a Map<String, Dog> is required.


> You can't tell whether T?? is the same type as T?

Huh? T?? would always be the same type as T?. `T | NULL | NULL == T | NULL`.


I think you have too many ? in there; you can't tell if T? is the same type as T unless you know if T is U? for some U. (This is true of untagged unions more generally, ? is just shorthand for | null.)

But T?? is always identical to T?.


They’re not direct support, but option types only work in languages with the right syntax and tooling to make them work. C union types, for example, aren’t really capable of providing a null replacement simply because they’re so complex to use.


They’re not direct support, but option types only work in languages with the right syntax and tooling to make them work.

I strongly disagree.

You don't need pattern matching syntax to use option types effectively. All you need are the right methods - map, flatMap, getOrElse etc etc. Any language with inheritance and dynamic dispatch can implement a good option type.

Even in languages with pattern matching, I never reach for it when dealing with options.


> Yes, option types are awesome. No, they are not nulls.

No, but they serve a superset of the semantic functions of nulls, better than nulls do.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: