> You have the signature for `useCache` wrong. I defined it above (`Cache -> a`)
Yes, sorry it's actually the line
maybeCache <- (getConfDirs >>= head >> initializeCache)
which shows the issue.
> but then the author eschews it in favor of... validation (with what appears to be some tricks to fool the compiler)!
I think the author is pretty clear about how they're using the terms 'validation' and 'parsing' in the post - validation functions do not return a useful value while parsers refine the input type and carry a notion of failure. The first two examples of parsers they give are:
nonEmpty :: [a] -> Maybe (NonEmpty a)
parseNonEmpty :: [a] -> IO (NonEmpty a)
you seem to be arguing that parseNonEmpty is validating because it throws an exception instead of returning Maybe (NonEmpty a) but this isn't true here since Maybe signals failure by returning Nothing errors within IO can be signaled with exceptions. The author hints at how these two parser types are related later on with:
checkNoDuplicateKeys :: (MonadError AppError m, Eq k) => [(k, v)] -> m ()
There are MonadError instances for both IO and Maybe so the general parser type is something like
MonadError e m => a -> m b
Admittedly this could have been made clearer if it was the intention and returning Maybe is preferable to throwing exceptions in languages like Haksell.
If you were translating this approach to other languages like Java or C# though you proabably would throw exceptions to indicate failure e.g.
interface Parser<A, B> { B parse(A input) throws ParseException; }
so I don't think your objection holds in general.
> I'd like you to define "strictly worse" here
I'm saying you would always prefer to be handed an instance of an `a` instead of a (Maybe a) since it's more precise. You can trivially construct a (Maybe a) from an a but you can't easily go in the other direction. You either need to produce a dfeault value or use a partial function like fromJust to obtain an 'a' from a 'Maybe a'. The motivation for the post is to show how using a more precise data type allow you to remove these from the rest of the code.
> But why are variables in this scope (`main`) so important
The issue doesn't happen in main, it happens throughout the rest of the program. The high-level structure is something like:
main :: IO ()
main = do
maybeDirs <- getConfigDirs
maybeDirs >>= restOfProgram
main only has to handle the parse failure and report any errors which will look similar regardless of whether getConfigDirs has type Maybe (NonEmpty FilePath) or IO (NonEmpty FilePath) (and throws an exception). But the representation of the directory list could be used anywhere in restOfProgram. Given a chain of applications fun1 -> fun2 -> ... -> funN, if funN accesses the file list with head and receives a (Maybe FilePath) there are three options:
1. Use fromJust since the list should be non-empty
2. Produce a default value
3. Propagate the Maybe in the return type of funN
Option 1 is messy, 2 is also unlikely for a low-level function and 3 forces fun1 to fun (N - 1) to either handle or propagate the partiality. Yes using >>= and <=< etc. can hide this plumbing but can be made unnecessary in the first place.