6 ms·
I think you're referring to this part of the `getConfigurationDirectories` action, which has type `IO (NonEmpty FilePath)`: case nonEmpty configDirsList of
by garethrowlands 5y ago
I think you're referring to this part of the `getConfigurationDirectories` action, which has type `IO (NonEmpty FilePath)`:
case nonEmpty configDirsList of
Just nonEmptyConfigDirsList -> pure nonEmptyConfigDirsList
Nothing -> throwIO $ userError "CONFIG_DIRS cannot be empty"
The "meaningful difference" you're looking for is the type of `getConfigurationDirectories`. The previous version had type `IO [FilePath] `, which _doesn't_ guarantee any configuration directories at all. It did indeed check the results and throw. But it doesn't guarantee that all the `[FilePath]` values in the program have been checked. There are neither tests nor proofs in this code. In contrast, with the revised version, you can be certain anywhere you see a `NonEmpty FilePath` it is indeed non-empty.
The code I've quoted that checks which case we have, is the only place that needs to handle that `Maybe`. Or maybe `main`, if we want to be more graceful. The author (I wouldn't say I know her but I know that much) does know how to chain maybes with bind but it's not necessary in this example code.
- kingdomcome50 5y agoMy point is that if you are not chaining `Maybe` then the utility of employing the construct is unobserved. The entire purpose of using `Maybe` is to relieve the client from the need to make checks at every call for a value that may (or may not) exist. If you intend to immediately "break out" of the monad and (even more specifically) throw an error, you might as well just use an `if`. I'm sure `main` could be written to "bind"/"map" `getConfigurationDirectories` with `nonEmpty`, `head`, and `initializeCache` in a way that puts the `throw` at the top-level (of course the above implementations may need to change as well). Unfortunately I'm not familiar enough with Haskell to illustrate it myself.
- lkitching 5y agoThe purpose of Maybe is to explicitly represent the possible non-existence of a value which in Haskell is the only option since there's no null value which inhabits every type. The existence of the monad instance is convenient but it's not fundamental. The type of getConfigurationDirectories could be changed to MaybeT IO (NonEmpty FilePath) to avoid the match but I don't think it would make such a small example clearer.
- kingdomcome50 5y agoThere are numerous ways to redesign the function signatures, but I would imagine the simplest would be (again, idk Haskell syntax): getConfigurationDirectories: unit -> Maybe [FilePath] nonEmpty: [a] -> Maybe [a] head: [a] -> Maybe a initializeCache: FilePath -> unit Notice `nonEmpty` isn't really necessary because `head` could to the work. The above could be chained into a single, cohesive stack of calls where the result of each is piped through the appropriate `Maybe` method into the next call in a point-free style. I cannot imagine how this wouldn't be clearer. e.g: maybeInitialized <- (getCofigurationDirectories >>= head >> initializeCache) That's the whole thing. Crystal clear. The big takeaway of "Parse don't validate" should be about the predominant use of the `Maybe` monad as a construct to make "parsing" as ergonomic as possible! Each function that returns `Maybe` can be understood as a "parser" that, of course, can be elegantly combined to achieve your result. My critique is exactly that unwrapping the `Maybe` immediately in order to throw an exception is kind of the worst of both worlds. I mentioned this in a sibling comment, but my sense is that the author is more concerned with have a concrete value (`configDirs`) available in the scope of `main` than best-representing the solution to the problem in code. It is a shame because I agree with the thesis.
- lkitching 5y agoOn the contrary the The NonEmpty type is fundamental to the approach in that example since it contains in the type the property being checked dynamically (that the list is non-empty). The nonEmpty function is a simple example of the 'parse don't validate' approach since it goes from a broader to a more restricted type, along with the possibility of failure if the constraint was not satisfied. The restriction on the NonEmpty type is what allows NonEmpty.head to return an a instead of a (Maybe a) and thus avoid the redundant check in the second example. The nonEmpty in your alternative implementation is only validating not parsing since after checking the input list is non-empty, it immediately discards the information in the return type. This forces the user to deal with a Nothing result from head that can never happen. Attempting to clean the code up by propagating Nothing values using bind is just hiding the problem that the validating approach avoids entirely.