4 ms·
How does it make it more complicated? In my view, you’re moving the potential for failure to a different place (the constructor), rather than changing some fun
by truculent 2y ago
How does it make it more complicated?
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?
- greener_grass 2y agoThe intuitive definition of NonEmptyList is: type NonEmptyList<'t> = 't * 't list But this cannot be passed to any function that expects a List<'t>. This is odd though, since intuitively, all non-empty lists are lists. See Rich Hickey's "Maybe Not" talk OOP solution is to use inheritance. Typical ML solution is to use type-classes. F# sits in an awkward middle-ground where neither is a perfect fit. I believe that dependently-typed languages solve this more elegantly. There's also the syntactic inconvenience of wrapping at construction, where in theory the compiler could figure it out for you. For example: let xs = 1 :: 2 :: 3 :: [] Here xs is non-empty, but we must tell the compiler: let xs = 1, 2 :: 3 :: [] TypeScript does a better job here (although a great cost!) What you end up with is massive code duplication or lots of extra function calls: xs |> NonEmptyList.toList |> List.map (fun x -> x + 1) |> NonEmptyList.unsafeFromList (I say this all as someone who really likes F#)
- munchler 2y agoYes, this is exactly what I was getting at. One ends up needing an explicit `toList` helper function that converts a non-empty list into a plain list. Pattern matching on a non-empty list is also inelegant, because it is implemented as a tuple, which creates a leaky abstraction.
- greener_grass 2y agoF# actually does have a good solution for pattern matching here, which is Active Patterns.
- josevalim 2y agoWe (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!
- yawboakye 2y agofor 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.
- greener_grass 2y agoDoes this break down if we want some other types? e.g. list(a) = empty_list() or singleton_list(a) or two_or_more_list(a)
- jerf 2y ago"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.)
- greener_grass 2y ago> You can take a List inherited from the NonEmptyList Shouldn't it go the other way? All NonEmptyLists are Lists, but not all Lists are NonEmptyList. So NonEmptyList inherits from List