-->

How do I call a constructor that may fail, especia

2019-08-12 08:40发布

问题:

I have a "public safe" that may fail with a (potentially informative) errors:

data EnigmaError = BadRotors
                 | BadWindows
                 | MiscError String

instance Show EnigmaError where
  show BadRotors = "Bad rotors"
  show BadWindows = "Bad windows"
  show (MiscError str) = str

configEnigma :: String -> String -> String -> String -> Except EnigmaError EnigmaConfig
configEnigma rots winds plug rngs = do
        unless (and $ [(>=1),(<=26)] <*> rngs') (throwError BadRotors)
        unless (and $ (`elem` letters) <$> winds') (throwError BadWindows)
        -- ...
        return EnigmaConfig {
                components = components',
                positions = zipWith (\w r -> (mod (numA0 w - r + 1) 26) + 1) winds' rngs',
                rings = rngs'
        }
    where
        rngs' = reverse $ (read <$> (splitOn "." $ "01." ++ rngs ++ ".01") :: [Int])
        winds' = "A" ++ reverse winds ++ "A"
        components' = reverse $ splitOn "-" $ rots ++ "-" ++ plug

but it is unclear how I should call this, particularly (and specifically) in implementing Read and Arbitrary (for QuickCheck).

For the former, I can get as far as

instance Read EnigmaConfig where
        readsPrec _ i = case runExcept (configEnigma c w s r) of
            Right cfg  -> [(cfg, "")]
            Left err -> undefined
          where [c, w, s, r] = words i

but this seems to end up hiding error information available in err; while for the latter, I'm stuck at

instance Arbitrary EnigmaConfig where
        arbitrary = do
                nc <- choose (3,4)  -- This could cover a wider range
                ws <- replicateM nc capitals
                cs <- replicateM nc (elements rotors)
                uk <- elements reflectors
                rs <- replicateM nc (choose (1,26))
                return $ configEnigma (intercalate "-" (uk:cs))
                                      ws
                                      "UX.MO.KZ.AY.EF.PL"  -- TBD - Generate plugboard and test <<<
                                      (intercalate "." $ (printf "%02d") <$> (rs :: [Int]))

which fails with a mismatch between the expected and actual types:

Expected type: Gen EnigmaConfig Actual type: Gen (transformers-0.4.2.0:Control.Monad.Trans.Except.Except Crypto.Enigma.EnigmaError EnigmaConfig)

How do I call a ("public safe") constructor when it may fail, particularly when using it in implementing Read and Arbitrary for my class?

回答1:

The Read typeclass represents parses as lists of successes (with failures being the same as no successes); so rather than undefined you should return []. As for losing information about what went wrong: that's true, and the type of readsPrec means you can't do much about that. If you really, really wanted to [note: I don't think you should want this] you could define a newtype wrapper around Except EnigmaError EnigmaConfig and give that a Read instance that had successful parses of configuration errors.

For Arbitrary you have a couple choices. One choice is so-called rejection sampling; e.g.

arbitrary = do
    -- ...
    case configEnigma ... of
        Left err -> arbitrary -- try again
        Right v  -> return v

You might also consider an Arbitrary instance to be part of your internal API, and use unsafe, internal calls rather than using the safe, public API for constructing your configuration. Other options include calling error or fail. (I consider these four options to be in roughly preference order -- rejection sampling, then unsafe internal calls, then error, then fail -- though your judgement may differ.)