F# has both a `head` and `tryHead` function to handle lists that may or may not be empty. In general, `tryFoo` is a good pattern for naming functions that might fail.
Having a separate NonEmptyList type might seem like a good idea in theory, but in my experience, it leads to code that is significantly more complicated.
Just to clarify for the crowd though. In F#, `List.head` throws an exception when it fails whereas `List.tryHead` returns an `option`, which returns `None` when it fails instead of an exception.
A general confusion of mine in Elixir is generally how libraries and functions treat errors. There's the common idiot of returning either `{:ok, ____}` or `{:error, ____}`, but what can be inside the error tuple is not always clear. The other thing is that sometimes a function can both throw an exception and also return a success tuple. Such cases are confusing to handle, and there's a large gap between handling cases like that and the philosophy of "let it crash", which I think is preached a little looser than it should actually be practiced.
I do like F#'s way of disambiguating the two situations. The only issue I have in F#, which actually exists in every language that I know of that has exceptions, is that there is no way to know, up front and clearly, what exceptions can be thrown by a given function. This is particularly frustrating in F#, which has fantastic pattern matching for exceptions. I wish there was exhaustive pattern matching in F# for exception handling, such that it would warn you that you have an unhandled exception in a try/with expression (https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...) but of course would allow for wildcard patterns.
> There's the common idiot of returning either `{:ok, ____}` or `{:error, ____}`, but what can be inside the error tuple is not always clear.
That's the result of Elixir being dynamic and not having built-in monads. You always have to check the docs/code.
> The other thing is that sometimes a function can both throw an exception and also return a success tuple.
I would say this is just poor design (though maybe someone could point me to an exemplary use of this?). As I see it (which is from how I've read it described and how most good lib do it):
- If function can error and the caller can do something about it, return :ok/:error.
- If the caller can't do anything about the error, raise.
- If the function can't fail, return a raw value.
It's true that it's a poor library implementation, but it's also a fairly natural consequence of a language with exceptions plus some other way of handling errors.
Even in a language like F#, just becauase a function returns an `option` type doesn't mean it won't also throw an exception sometimes. However, I don't necessarily think pure functional languages solve this either. If a Haskell function returns an option, you have little idea as to where the error originated. There are error types such as result types, which F# has as well, but then that's basically back to exceptions, perhaps even as a more limited form.
I'm curious if there's a language that's really nailed error handling. Erlang/Elixir have their supervisors and functional languages have pattern matching on option types, result types, and exceptions, but surely there's a way to improve on that.
Java has checked exceptions and they are used, e.g. in Android. But it seems to be the exception (heh) rather than the rule.
IMO the problem is proper exception handling with checked exceptions and wrapping each function (or at least small blocks) in try catch is just so insanely verbose that even though it is possible to get error handling as good as something like Rust, nobody actually does it in practice.
In my view, you’re moving the potential for failure to a different place (the constructor), rather than changing some fundamental property or introducing new complexity.
Is it handling the construction of these types you find complicated? And is it simply not worth the guarantees?
We (Elixir) would rather define lists on top of non empty lists:
list(a) = empty_list() or non_empty_list(a)
So you should pass non-empty lists everywhere a list is expected. But you can’t pass a list where a non-empty one is expected.
But overall, you are right: our concern is exactly all of the extra function calls that may now suddenly become necessary (and the tension mentioned in the article). We will review our design decisions as we keep on rolling out the type system!
for when one expects a list to be non-empty, i think there’s strong argument in favor of an enforcement from the type checker, given that the prove will very likely be necessary. if not in the application code then in the tests.
"OOP solution is to use inheritance. Typical ML solution is to use type-classes."
Yes, type classes can "work" to help a NonEmptyList degenerate to a normal List of some sort, if the function accepting the list accepts the type class instead of a hard-coded List. Unfortunately, at least for this exact task, taking hard-coded types is pretty common. I've sometimes wondered about the utility of a language that provided all of its most atomic types solely as typeclasses within its standard library, so that calling for a "List a" or "[a]" automatically was turned into the relevant type class.
Inheritance doesn't actually work here. I assume you mean inheriting a NonEmptyList from some sort of List, from the perspective of a user facing a language that has a standard List and they want to create a NonEmptyList that things taking List will accept. Unfortunately, that is a flagrant violation of the Liskov Substitution Principle and will create architecturally-fragile code.
Compilers can't enforce the LSP (with anything short of the dependently typed code you mention), so you can bash out a subclass that will throw an exception if you try to take the last element out of a NonEmptyList or violate the rules some other way, and if you pass your new NonEmptyList to something that happens to not do anything broken, you may get away with it, but by the standards of OO theory you're definitely "getting away" with something, not solving the problem.
I haven't studied this extensively beyond just thinking here for a moment, but I don't think you can LSP-legally go the other way either. A subclassed List can't revoke a parent's NonEmptyList property that the list is guaranteed to not be empty. Again, you can bash the methods into place to make it work, but as this is a very basic standard library sort of thing for a language it really needs to be right.
Edit: Yes, it's certainly illegal. You can take a List inherited from the NonEmptyList, have it be empty, but you have to be able to pass it to something accepting a NonEmptyList, but it will then be empty. So you can't LSP-legally inherit either way.
(This is one of the "deep reasons" why inheritance is actually not a terribly useful architectural tool. It technically breaks really, really easily... like, probably most non-trivial uses of inheritance in most code bases is actually wrong somehow, even if never happens to outright crash. We tend to just code past the problem and not think about it too hard.)
Neither direction works. More directly (since I was working it out as I typed above):
A NonEmptyList promises that its .Head method will always produce a value. An inherited List can not maintain that property, it must add either an error return or a possible exception (which is the same thing from this point of view), and so violates the LSP.
A List promises that if it has an element, you can remove it and have another List, whether by mutation or returning a new List. A NonEmptyList breaks that promise. If that sounds like a "so what", bear in mind that "removing an element" includes things like a "Filter" method, or a "split" method, or any of several other such methods beyond just iteration that a List is likely to have that a NonEmptyList is going to need a different type signature and/or exception profile to implement properly.
You could define a bare-bones superclass for both of them that allows indexing, iteration, appending, and a length method, without much else, and that does work. However, if you start trying to get very granular with that, across more data structures, you'll start to need multiple inheritance and that becomes a mess really quickly. There's a reason that, for instance, the C++ STL does not go the "inheritance hierarchy" route for this stuff.
Like I said, inheritance done properly is really restrictive. We often do a lot of sweeping under the rug without even realizing it, and that "works" but it still eats away at the architecture, all the more so if the people involved don't even realize what they are doing.
> A List promises that if it has an element, you can remove it and have another List, whether by mutation or returning a new List. A NonEmptyList breaks that promise.
I don't follow. Remove the head from a NonEmptyList and the tail will be a List. It might not be a NonEmptyList, but that's not the contract of List.
I didn't spell it out adequately, only implied it, but I am talking about a fully loaded List, not one that just barely works, e.g., it has filter, it has all the other things you'd expect from a List. I did discuss the "just barely works" case at the end.
But the nonempty list never has an element, so we don't need to worry about the type mutation of removing an element from it. Filter just returns a nonempty list.
Having a separate NonEmptyList type might seem like a good idea in theory, but in my experience, it leads to code that is significantly more complicated.