问题
In my Python package I have a function I use to create validated instances of my class, with something like
@staticmethod
def config_enigma(rotor_names, window_letters, plugs, rings):
comps = (rotor_names + '-' + plugs).split('-')[::-1]
winds = [num_A0(c) for c in 'A' + window_letters + 'A'][::-1]
rngs = [int(x) for x in ('01.' + rings + '.01').split('.')][::-1]
assert all(name in rotors for name in comps[1:-1])
assert comps[-1] in reflectors
assert len(rngs) == len(winds) == len(comps)
assert all(1 <= rng <= 26 for rng in rngs)
assert all(chr_A0(wind) in LETTERS for wind in winds)
#...
and I would like to enforce the same behavior in Haskell. But doing so in the same way — with assertions — does not work, because Haskell assertions are disabled in general (unless certain compiler flags are set). For example, in something like
configEnigma rots winds plug rngs =
assert ((and $ (==(length components')) <$> [length winds', length rngs']) &&
(and $ [(>=1),(<=26)] <*> rngs') &&
(and $ (`elem` letters) <$> winds') &&
(and $ (`M.member` comps) <$> tail components'))
-- ...
where
rngs' = reverse $ (read <$> (splitOn "." $ "01." ++ rngs ++ ".01") :: [Int])
winds' = "A" ++ reverse winds ++ "A"
components' = reverse $ splitOn "-" $ rots ++ "-" ++ plug
can't be relied on to work because the assertions will be removed in most contexts.
What is an idiomatic and reliable way to force all my instances to be validated in Haskell (using a "public safe" constructor)?
回答1:
The normal thing is to express failure explicitly. For example, one might write
configEnigma :: ... -> Maybe ...
configEnigma ... = do
guard (all (((==) `on` length) components') [winds', rngs'])
guard (all (inRange (1,26)) rngs')
guard (all (`elem` letters) winds')
guard (all (`M.member` comps) (tail components'))
return ...
where
...
You might even consider upgrading from Maybe
to Except Error
for some custom-defined type Error
, to communicate to the caller what it was that went wrong during construction. Then instead of guard
, you could use a construction like:
unless (all (inRange (1,26)) rngs') (throwError OutOfRange)
The caller to configEnigma
will have to express how to handle failures. For Maybe
, this looks like
case configEnigma ... of
Just v -> -- use the configured enigma machine v
Nothing -> -- failure case; perhaps print a message to the user and quit
while with Except
you get information about what went wrong:
case runExcept (configEnigma ...) of
Right v -> -- use the configured enigma machine v
Left err -> -- failure case; tell the user exactly what went wrong based on err and then quit
来源:https://stackoverflow.com/questions/33713212/how-do-i-validate-arguments-to-a-haskell-public-safe-constructor