> 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:
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).
The 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.
> The requirement[0] from the post is for a non-empty list of file paths
I'm sure you could imagine my example record containing more keys no?
The requirements from the post are arbitrary and could (should) be anything that best-illustrates the thesis of the post. For example by choosing a representation of their "configuration" that suffers from this silly problem of containing unknown content after parsing, the author introduces the whole `NonEmpty` gymnastics. It's totally avoidable. The irony is that the author was so close to getting it right!
> You check the list is non-empty but this information is not available anywhere else in the program
The function in my example does statically define that the returned list is non-empty. A "parser" would maybe return the non-empty list if parsing was successful. That's how parsers work.
[0] The requirement from the post is, in fact, to only have a single file path. That is the only data actually being used (i.e. required). The other intermediate data structures are a choice of the author.
> The requirements from the post are arbitrary and could (should) be anything that best-illustrates the thesis of the post
The post is showing how using more expressive types can simplify your code through a very simple example. You started out saying the NonEmpty type is unnecessary and now you're complaining it's not complicated enough, so it's not clear what your actual objection is here.
> The function in my example does statically define that the returned list is non-empty
The type given in your example was
getConfDirs :: Maybe [FilePath]
this is inhabited by (Just []) so no it doesn't statically guarantee that.
> A "parser" would maybe return the non-empty list if parsing was successful
That's exactly what the nonEmpty function in the post does:
> The post is showing how using more expressive types can simplify your code
This is called "using a type system" and has nothing to do with parsing or validation. You just don't "get it" I suppose. I feel like we are talking right past each other. You are so hung up on the types that you are just failing to take in the essence of what's going on.
I'll say this one more time: the specific type returned by `getConfDirs` is completely irrelevant -- other than whether it is wrapped in `Maybe` or not (because this best-illustrates parsing vs. validation). The returned type is an implementation detail that is chosen by the author. It is not necessary or "required" that we parse a string into a list (read that again). It's really quite simple:
// this is parsing
getConfDirs :: Maybe Config
// this is validation
getConfDirs :: Config // might throw
How these functions are implemented is not relevant. The irony is that choosing a list representation makes for a good example of parsing precisely because we can't know how many elements are in the list. That is, it gives the author the opportunity to further-illustrate parsing by:
// more parsing
maybeCacheDir <- (confDirs >>= first) // or second, third, fourth, etc.
The author's example code is NOT illustrating parsing. Period. They are essentially illustrating a constructor named `getConfigurationDirectories` -- which is the most classic case of validation imaginable (TS):
// 1. This is their first example
class ConfigurationDirectories {
dirs: FilePath[];
constructor(env) {
let dirs = env("config").split(",");
if (dirs.length < 1) throw "Error!";
this.dirs = dirs;
}
get cacheDir() {
if (this.dirs.length < 1) throw "Cannot happen!";
return this.dirs[0];
}
}
// 2. This is their "fixed" example with all sorts of unnecessary "NonEmpty" gymnastics because they have chosen the wrong abstraction
type NonEmpty<T> = [T, ...T[]];
const eg: NonEmpty<string> = []; // error
class ConfigurationDirectories {
dirs: NonEmpty<FilePath>;
constructor(env) {
let dirs = env("config").split(",");
if (dirs.length < 1) throw "Error!";
this.dirs = dirs as NonEmpty<FilePath>;
}
get cacheDir() {
// now the compiler also knows dirs is not empty
return this.dirs[0];
}
}
// 3. This is BETTER than their "fixed" example because it is even simpler
class ConfigurationDirectories {
cacheDir: FilePath;
constructor(env) {
let dirs = env("config").split(",");
if (dirs.length < 1) throw "Error!";
this.cacheDir = dirs[0];
}
}
// usage
const config = new ConfigurationDirectories(getEnv());
const cache = initializeCache(config.cacheDir);
You see how the above translates to the author's code? Nothing above qualifies as "parsing". Their examples are defining a function with a guard that validates the input. The author then get confused and decides to go on this side-quest of how to trick the compiler because the first function wasn't actually accomplishing their goal (they have to validate twice!). But you know what would have avoided all of it? Parsing (i.e. actually using the types to simplify their code). Simple. Linear. Fail-safe. Parsing.
I'm not really sure why you are dying on this hill. You can't "win" this argument. The best you can accomplish is to learn something yourself through this discussion. It's just a matter of fact that the author is (mis)using `Maybe` to the detriment of their examples. And that isn't an attack on the author! You needn't defend them. We've all written code that wasn't perfect. This is just another instance.
Maybe if the title were something like, "How to validate with static guarantees in Haskell" I wouldn't have said anything...
* I thought you were referring to my TS example (which does statically define the return-type to be non-empty)
> You are so hung up on the types that you are just failing to take in the essence of what's going on.
The post is about type-driven design, which is about representing invariants in the type system where possible. The post is very clear about this. The example chosen to illustrate it is a very simple one where a list is augmented with an extra property (non-emptiness). The exact representation is not important, so yes they could have created their own custom type instead of using (NonEmpty a) but this is beside the point. You have now made two attempts to 'improve' this representation, first by just using a plain list (which the post explicitly rejects) and now by just using a single 'cache dir' instead. You can't 'simplify' the solution by just throwing away half the requirements - it's a collection of items which must also be non-empty.
> The author's example code is NOT illustrating parsing. Period
Once again, the author is very clear about what they mean by 'validation' and 'parsing':
The difference lies entirely in the return type: validateNonEmpty always returns (), the type that contains no information, but parseNonEmpty returns NonEmpty a, a refinement of the input type that preserves the knowledge gained in the type system.
The entire point of 'parsing' in this approach is to obtain a refinement of the input type in the representation. Your 'better' example is not a refinement of a list.
You appear to be insisting that validation is just anything that throws exceptions, but this is wrong - validation is when the properties being checked are not reflected in the input type. Parsers have to be able to signal errors, and exceptions is one of the ways of doing that. This is why your previous example of 'parsing' is just validating:
The type of this expression is just `[String]` which does not guarantee the non-emptiness being checked. If you have another definition of validation vs parsing you need to state it clearly, because your counterexamples do not contradict the definition in the post.
> Their examples are defining a function with a guard that validates the input. The author then get confused and decides to go on this side-quest of how to trick the compiler because the first function wasn't actually accomplishing their goal (they have to validate twice!). But you know what would have avoided all of it? Parsing
This is literally what the post is showing by switching
you keep insisting this is 'not parsing' but the post explains why they think it qualifies and you haven't given your own definition which contradicts it.
> It's just a matter of fact that the author is (mis)using `Maybe` to the detriment of their example
There's no mis-use of Maybe in the post, and as I've already explained, the point of the post is to eliminate Maybes. It's only used in two places - to represent the partiality of the head and nonEmpty functions.