7 ms·
This comes up again and again in one form or the other, yet new languages still seem to be making the same mistake. Of all languages I've touched, Rust seems to
by x3ro 7y ago
This comes up again and again in one form or the other, yet new languages still seem to be making the same mistake. Of all languages I've touched, Rust seems to be the only one that mostly circumvents this problem. Are there other good examples?
- gameswithgo 7y agorust, f#, ocaml, latest version of c# has an option to sort of get rid of nulls, zig
- hawkice 7y agoHaskell, notoriously. I believe it pioneered the ergonomics of the alternatives used elsewhere.
- cmrdporcupine 7y agoAFAIK Standard ML predates Haskell and it has an option type.
- dunefox 7y agoML is even older than SML and has algebraic data types.
- jrockway 7y agoI assume two reasons, efficiency and because an efficient implementation of mutable state would have the same problem. Right now, a single sentinel value makes a pointer null or not null (0x0 is null, everything else is not null). This is exactly how you'd implement a stricter type, like "Maybe". Encoded as a 64-bit integer, "Nothing" would be represented as 0x00000000 and "Just foo" would be represented as 0xfoo. No object may be stored at the sentinel value, 0x00000000. Exactly the same as what we have now, and provides no assurances that 0xfoo is actually a valid object. Meanwhile, Haskell which "doesn't have null" crashes for exactly the same reason your non-Haskell program crashes with a null pointer exception: f :: Num a => Maybe a -> Maybe a f (Just x) = Just (x + 41) This blows up at runtime when you call f Nothing, because f Nothing is defined as "bottom", which crashes the program when evaluated. It's exactly the same as langages with null pointers: func f(x *int) *int { result := *x + 41 return &result } And the solution is the same, your linter or whatever has to tell you "hey maybe you should implement the Nothing case" or "hey maybe you should check the null pointer". Where I'm going with this is that you need to develop entirely new datatypes and have an even stricter type system than Haskell. Maybe Rust is doing this, but it's hard. We all know null is a problem, but calling null something else doesn't make the problems go away.
- anderskaseorg 7y ago> It's exactly the same as langages with null pointers: Four huge differences: 1. You don’t need to pass around ‘Maybe a’ everywhere. If null isn’t expected as a possible value (which usually it isn’t), you just pass around ‘a’, and when you do use ‘Maybe’ it actually means something. 2. The Haskell compiler can, and does (with -Wall), tell you that your pattern match is non-exhaustive. You don’t need a separate “linter or whatever”. This is possible because the needed information is present in the type system, and doesn’t need to be recovered with a complicated and incomplete static analysis pass. 3. If you do this anyway, the error is thrown at exactly the point where ‘Maybe a’ is pattern-matched, not at some random point several function calls later where your null has already been coerced into an ‘a’. 4. This program is defined to throw an error; it’s not undefined behavior like in C that could result in something weird and unpredictable happening later (or earlier!). Also, Rust optimizes away the tag bit of ‘Option’ under common circumstances; for example, ‘None: Option<&T>’ (an optional reference to ‘T’) is represented internally as just a null pointer, which is safe because ‘&T’ cannot be null.
- jrockway 7y ago> You don’t need to pass around ‘Maybe a’ everywhere. You don't need to pass pointers around everywhere. Languages with null still have value types that cannot be null. > You don’t need a separate “linter or whatever”. Optional compiler flags count as "whatever" to me. > it’s not undefined behavior like in C that could result in something weird and unpredictable happening later (or earlier!) C++ doesn't define this, but the OS does (and even has help from the CPU). Anyway, my TL;DR is that it's easy to have a slow program that passes everything by value, or east to have a fast program that uses pointers or references. Removing the special case of null is meaningless, because you can still have a pointer to 0x1 which is just as bad as 0x0, probably. This goes back to my original answer to the question "why don't more languages get rid of null" which was "it's harder than it looks." I think I'm right about that. If it were easy, everyone would be doing it.
- temac 7y ago> Languages with null still have value types that cannot be null. Not all languages. > C++ doesn't define this, but the OS does (and even has help from the CPU). That's not how it works anymore, because C / C++ front-ends interacting with the optimizers are yielding too "optimized" results. See the classic https://t.co/mGmNEQidBT https://t.co/mGmNEQidBT
- deepaksurti 7y agoSwift with optional and optional chaining. [1] [1] https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html https://docs.swift.org/swift-book/LanguageGuide/OptionalChai...
- zozbot234 7y ago> Rust seems to be the only one that mostly circumvents this problem. The Rust hype is getting ridiculous here. There are plenty of languages with non-nullable references as first-class, and optionals for the nullable case. (...And I say this as a Rust fan myself, for what it's worth.)
- amelius 7y agoImho, Rust is an awkward language because it positions itself as a systems language but it makes low-level stuff more difficult (there's even a book teaching how to implement doubly linked lists in Rust [1]), hence prone to mistakes. At the same time, people are using Rust to build non-systems programs, where other languages would be more appropriate (e.g. those with garbage collectors). I don't think it is a good idea that Rust is promoted as the language that will rule them all; in my opinion, it is still a research language. Linus Torvalds said the following about Rust [2]: [What do you think of the projects currently underway to develop OS kernels in languages like Rust (touted for having built-in safeties that C does not)?] > That's not a new phenomenon at all. We've had the system people who used Modula-2 or Ada, and I have to say Rust looks a lot better than either of those two disasters. > I'm not convinced about Rust for an OS kernel (there's a lot more to system programming than the kernel, though), but at the same time there is no question that C has a lot of limitations. [1] https://rust-unofficial.github.io/too-many-lists/ https://rust-unofficial.github.io/too-many-lists/ [2] https://www.infoworld.com/article/3109150/linux-at-25-linus-torvalds-on-the-evolution-and-future-of-linux.html https://www.infoworld.com/article/3109150/linux-at-25-linus-...
- zozbot234 7y ago> there's even a book teaching how to implement doubly linked lists in Rust [1] Doubly-linked lists are an awkward example because the "safety" of a doubly-linked list as a data structure involves fairly complex invariants that Rust can't even keep track of at this point, much less check independently. These things are exactly why the unsafe{} escape-hatch exists and is actively supported. But just looking at the amount of unsafe code in common Rust projects should suffice to figure out that this is not the common case, at all. > At the same time, people are using Rust to build non-systems programs, where other languages would be more appropriate (e.g. those with garbage collectors). Garbage collectors are good for one thing, and one thing only: keeping track of complex, spaghetti-like reference graphs where cycles, etc. can arise, perhaps even as a side effect of, say, implementing some concurrency-related pattern. Everything else is most likely better dealt with by a Rust-like system with optional support for reference counted data. That's without even mentioning the other advantages that a Rust-like ownership system provides over a GC-only language. See e.g. https://llogiq.github.io/2020/01/10/rustvsgc.html https://llogiq.github.io/2020/01/10/rustvsgc.html this recent post for some nice examples.
- progval 7y ago> Rust seems to be the only one that mostly circumvents this problem. Are there other good examples? Rust is not the first one to have an Option type; it's a common feature of functional languages because they have ADTs ( https://en.wikipedia.org/wiki/Algebraic_data_type https://en.wikipedia.org/wiki/Algebraic_data_type )
- masklinn 7y ago> Rust seems to be the only one that mostly circumvents this problem. Are there other good examples? Swift, Kotlin, and of course older languages of a functional bend like MLs, Haskell, Idris, Scala, … Some are also attempting to move away from nullable references (e.g. C#), though that is obviously a difficult task to perform without extremely severe disruptions.
- the_alchemist 7y agoScala happily accepts null as it is the bottom type for AnyRef and needed for jvm compatibility. Kotlin has a compiler check that enforces it, Scala does not.
- gmartres 7y agoIt's coming to Scala too: https://dotty.epfl.ch/docs/reference/other-new-features/explicit-nulls.html https://dotty.epfl.ch/docs/reference/other-new-features/expl...
- rubyn00bie 7y agoI really love(d) Scala for introducing me to the whole idea of Optionals. I wish for the life of me I felt like I could approach Scala at a time when it wasn't going through huge flux (I have shitty luck). I spent a good amount of time pre-version 2.10 :( and then recently went to have a look but saw Dotty (version 3.0?) coming by the end of 2020 and I was like "well, FML, time to wait a few more years and try again." Anyone have any tips for using the Scala ecosystem effectively these days? Should I just wait for 3.0? Is it going to be a long winding road of breaking changes until a "3.11" version? Is there a good resource for what folks are using it for these days? It seems like all the projects I used to know are ghostly on Github (but that could also be the fact it has been quite a few years, heh). Or do most folks just pony-up and use plain ol' Java libraries while writing their application/business logic in Scala?
- deleted 7y ago[deleted]
- augusto2112 7y agoFunctional programming languages have been doing it for ages. Most "newer" statically typed languages also have it (Swift, Kotlin, Rust) by default. And old languages had it bolted on (C# 8, Java 8, C++ 17). I think at this point basically everyone has realized null by default is a terrible idea.
- masklinn 7y ago> And old languages had it bolted on (C# 8, Java 8, C++ 17). C#: actually true, you can switch over to non-nullable reference types Java 8: meeeh, it provides an Optional but all references are still nullable, including references to Optional. There are also @Nullable and @NotNull annotations but they're also meh, plus some checkers handle them oddly[0] C++17: you can deref' an std::optional, it's completely legal, and it's an UB if the optional is empty. Despite its name, std::optional is not a type-safety feature, its goal is not to provide for "nullable references" (that's a pointer), it's to provide a stack-allocated smart pointer (rather than have to allocate with unique_ptr for instance). [0] https://checkerframework.org/manual/#findbugs-nullable https://checkerframework.org/manual/#findbugs-nullable
- davidgay 7y agoThe reason they come up again and again is that it's hard to design an imperative language without them (try, assuming you want to provide generic user-defined data structures that allow for cycles). As a result, calling them a "mistake" is reasonably dishonest, as it implies there was an obvious, better alternative.
- tene 7y agoCan you give some more details on what the design problem is here? It seems to me that nullable references are isomorphic to having an option type with non-nullable references, but prevent accidental unchecked dereference. What are some of the difficulties that you'd expect to come up if you took an imperative language with nullable references and replaced them with options of non-nullable references?
- davidgay 7y agoI don't consider 'option' types to have interesting semantic differences with nullable types. YMMV. But beyond that, the absence of nullable references (really, a valid default value for every type) is a problem for record/object/struct initialisation - you either have to provide all values at allocation time, or attempt to statically check that the object is fully initialised before any use - Java has rules to that effect for 'final' fields, and they are both broken and annoying (less broken rules would likely just be more annoying).
- tene 7y agoThe difference is that you can't accidentally use an option as a pointer without checking it first, and when your APIs specify a non-nullable pointer you can rely on the callers to have checked for null. When you're reading or writing a function that accepts a non-nullable reference, you never have to worry about whether the argument is null or not. It's easier to get right, constrains the scope of certain types of errors. If you get things wrong, and unwrap the option without checking, you get an assert failure at that location, rather than potentially much later on when the pointer is used. The whole point is that Option<&Foo> replaces nullable &Foo, so your record/object/struct member is Option<&Foo> and the default value for it is None. Option<&Foo> even has the same runtime representation for nullable &Foo, as Option<&Foo> uses NULL to represent None. It's just a different way of representing nullable references, but with semantics that make it easier to track null-checked vs nullable references, impossible to accidentally get it wrong and derefence a nullable pointer you mistakenly assumed was already checked, and better errors when you do make mistakes.
- pkulak 7y agoKotlin
- ronanyeah 7y agoElm is a great JS replacement on the frontend.