1

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?

orome
  • 45,163
  • 57
  • 202
  • 418

1 Answers1

2

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.)

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • Since `read` should fail completely, and since want to inform, rather than `undefined` or `[]`, what about `error (show err)`? Also in the case of `Arbitrary`, since I'm generating only valid values for the arguments, I want it to fail immediately rather than quietly try another; so `error (show err)` rather than `arbitrary`? – orome Nov 15 '15 at 22:13
  • 1
    @raxacoricofallapatorius If you want to write a parser with informative errors, you are free to; you just can't (shouldn't) call it `read`. – Daniel Wagner Nov 15 '15 at 23:13
  • Ah, I see. I missed that in the docs for `Read`: no matter what the cause, `read` should fail with "no parse" (and `readsPrec` should return `[]`, as you say: ["If there is no successful parse, the returned list is empty."](http://hackage.haskell.org/package/base-4.8.1.0/docs/Prelude.html#v:readsPrec)). That's too bad for the user, imv, (since information is lost); but those are the rules I guess. – orome Nov 16 '15 at 13:30
  • For `Arbitrary EnigmaConfig ` though, if I'm generating arguments that all out to be valid, I certainly don't want to do rejection sampling (as cool as that is): I just want to fail (I think): it would be a true coding error. – orome Nov 16 '15 at 13:32
  • (Note that the loss of info in `readsPrec`, and potentially other cases , can be avoided if the constructer fails with `error` – see my recent comment to the [related answer](http://stackoverflow.com/a/33713265/656912).) – orome Nov 16 '15 at 13:41