5 ms·
The entire point of Maybe is to imbue some type 'a' with an extra value - Nothing - along with a tag about which case you have. So (Maybe a) is always inhabited
by lkitching 5y ago
The entire point of Maybe is to imbue some type 'a' with an extra value - Nothing - along with a tag about which case you have. So (Maybe a) is always inhabited by more values than 'a', and that is the sense in which a variable of type a is 'more precise' than one of (Maybe a). I'm not saying Maybe is bad in any way - as you point out sometimes you do have to deal with the possibility of not having a value e.g. looking up a key in a map, looking up a user from a database etc. In Haskell there's no 'null' value which inhabits each type so you have to use Maybe, but even in languages like C# or Java where reference types all contain null I would still prefer to use Maybe/Optional to be explicit about the possibility. I don't think we disagree here. But at any point in a program you would always prefer to receive an 'a' over a (Maybe a) if you had the choice since there are fewer cases to deal with. This is the same reason languages like C# are adding support for non-nullable reference types.
Type-driven design is based around encoding invariants as much as is practical in the type system (what constitutes 'practical' is constrained by the type system you're using). The (NonEmpty a) type is just used to demonstrate a very simple example of this principle. In the same way that type 'a' is smaller than the type (Maybe a), so (NonEmpty a) is smaller than a [a] which means the operations on it are similarly more precise, which shows up in the two version of head:
head :: NonEmpty a -> a
head :: [a] -> Maybe a
But this is just one example - you could replace it with different representations of a user in a web service
type User {name :: String}
type User = JsonValue
and the consequent difference in the types of the accessor for the name:
getName :: User -> String}
getName :: JsonValue -> Maybe String
Far from being a 'sideshow' this is the main point of the approach - using a more precise representation makes all the operations on it similarly more precise globally throughout the program.
In your post the argument to restOfProgram has type [FilePath] but in the post it is (NonEmpty FilePath) so you need to handle the potential non-emptiness of the list everywhere you try to access it, either by propagating missing values to a higher level or using 'unsafe' functions like fromJust. It's defensible to prefer using a simpler representation type and dealing with the imprecision, but it's not doing the same thing - the types for a lot of the internals of your program will be quite different. This is probably the main philosophical difference with Clojure which prefers to use a small number of simple types along with dynamically checking desired properties at the point of use, something which tools like spec and schema make quite convenient. But people use static languages because of the global property checking, so it seems odd to me to endorse explicit modelling of missing values with Maybe while rejecting doing the same thing for non-emptiness since they are both lightweight approaches.
The insight of the original post is that if you choose to try make your types precise in this way (and most Haskell programers would I believe) then the process of checking the properties you want to enforce from a less-precise representation is inseperable from the process of converting into the narrower representation.
This narrowing process could fail and must therefore encode the representation for the failure case. Your insistence that Maybe should be used as the one true failure representation is wrong I think, throwing exceptions in Haskell is rare but but they could have also chosen (Either String) for example. Maybe isn't even a particularly good representation since it doesn't contain any way of describing the reason for the failure, just that it happened. I agree it would have been nice to see an example of parser composition using <=< etc. would have been useful there but it's not the main point of the article.
- kingdomcome50 5y agoI understand the purpose of `Maybe`. > In your post the argument to restOfProgram has type [FilePath] but in the post it is (NonEmpty FilePath) so you need to handle the potential non-emptiness of the list everywhere you try to access it This is what I'm talking about. You are wasting energy on this line of thinking. Sure the author chose to parse a string into a list which then introduces the possibility of that list being empty. But we could have just chosen a different abstraction to hold our configuration that didn't suffer from this problem. Say: getConfiguration :: () -> Maybe { cache :: FilePath } Now it's always non-empty. Don't get stuck on some intermediate representation. Again, I am uninterested in the details of the particular format of some input. My interest (and the thrust of this discussion) is about how to handle an input that might not exist. Specifically in terms of "parsing instead of validating". > Your insistence that Maybe should be used as the one true failure representation I cannot stress this enough (I've said this at least twice now), I am not arguing that `Maybe` is "the one true way". I am arguing that the author is failing to exemplify how to parse your inputs vs. validating them. I am arguing that the code they wrote to help substantiate and illustrate their point about parsing accomplishes no such thing. It actually shows how to validate an input in a way that is confusing and no different than (in TS): // returns a non-empty list of string getConfigurationDirectories: () => [string, ...string[]] = () => { const dirs = getEnv("dirs").split(","); if (dirs.length < 1) throw "ERROR"; return dirs; } The above is not best-understood as a "parser". The above is validating the input. Trying to redefine "parsing" to mean "the result has a different return type" helps no one, and introducing `Maybe` into their example (while on the right track) isn't really necessary because they aren't using the `Maybe` (other than maybe as a crutch to satisfy the compiler).
- lkitching 5y agoThe requirement from the post is for a non-empty list of file paths - your Cache representation only contains a single item so is obviously not suitable. In case it's not obvious: head is not the only operation that might be required and the author isn't using (NonEmpty a) as a wrapper around a single value. The requirements for the configuration are stronger than those provided from the input and the configuration type used by the program encodes that property in the type. That property is then enforced globally throughout the entire program and only needs to be checked once at the top level. > The above is not best-understood as a "parser". The above is validating the input Yes the example you gave is an example of validating in the author's formulation because it does not enforce the property it's checking in the return type. You check the list is non-empty but this information is not available anywhere else in the program. A parser would return a (NonEmpty String) as its result since that does enforce the constraint. > Trying to redefine "parsing" to mean "the result has a different return type" That's not a redefinition of parsing, that's what a parser is.