pcapriotti / optparse-applicative

Applicative option parser
BSD 3-Clause "New" or "Revised" License
914 stars 116 forks source link

Support failure in Parser, thus support errors depending on multiple options #281

Closed amigalemming closed 6 years ago

amigalemming commented 6 years ago

i like to generate an error that depends on the values of multiple options. E.g. I have options --width and --height and want to assert that one of the two values is even. A function with this signature would solve the problem for me:

join :: Parser (Either String a) -> Parser a

Since it is restricted to Either it does not turn Parser into a monad. I could then write:

OP.join $
OP.liftA2
   (\w h ->
      if mod (w*h) 2 == 0
         then Right (w,h)
         else Left "One of width or height must be even")
   optWidth optHeight
HuwCampbell commented 6 years ago

Hi,

First up I'm not sure if this sort of validation should be the job of optparse-applicative. I wouldn't know how to express this relationship in a usage summary for instance. Nevertheless, I'll answer as best I can.

Your function definitely inspects the value of a Parser, so is morally and practically "monadic". You can therefore use the ParserM type to get most of the way there, but we don't provide a way to emit custom error messages at this level of parser evaluation; i.e., ParserM doesn't have a valid instance for MonadFail.

I'll have a think about it, but I don't think this can be done satisfyingly without changing the core Parser type, which is something I am quite hesitant to do.

I would suggest handling this outside of optparse as a stop gap.

Cheers, Huw

amigalemming commented 6 years ago

On Fri, 1 Dec 2017, Huw Campbell wrote:

First up I'm not sure if this sort of validation should be the job of optparse-applicative. I wouldn't know how to express this relationship in a usage summary for instance. Nevertheless, I'll answer as best I can.

I understand that the addition of 'join' requires more effort than can be seen from the outside. I don't see the summary as a problem, I would simply ignore 'join' for the summary. Other value constraints are also neither shown in summary nor in the help page. However, a problem might be the error reporting: Currently, execParser says: "option --xxx: this and that problem". If there are multiple options involved it would have to say: "options --xxx and --yyy or --zzz: this and that problem".

My motivation for 'join' is that I want to encapsulate option parsing and return completely checked values such that I can use the options in different contexts. For the size example I can choose between "--width=6 --height=4" and "--size=6x4". The second one allows the check I want, and the first one does not.

HuwCampbell commented 6 years ago

For the size example I can choose between "--width=6 --height=4" and "--size=6x4"

The second sounds like the best idea to me, it completely captures the invariant within one ReadM, and thus allows for the best error reporting. It's also quite similar to imagemagick commands IIRC.

I don't think there's anything really actionable from this issue right now, so I will close. I have however been thinking about more obscure failure cases, so will take this conversation on board in future. It really comes down to finding a good MonadFail instance for ParserM (which is the "hidden" monadic interface to optparse).

Ta.

amigalemming commented 6 years ago

On Sat, 2 Dec 2017, Huw Campbell wrote:

I don't think there's anything really actionable from this issue right now, so I will close. I have however been thinking about more obscure failure cases, so will take this conversation on board in future. It really comes down to finding a good MonadFail instance for ParserM (which is the "hidden" monadic interface to optparse).

I thought a bit about it and think that you can easily get the 'join' and 'fail' feature without touching the current Parser data type. You could define a wrapper:

newtype ParserFail a = ParserFail (Parser (Either String a))

fail :: String -> ParserFail a fail str = ParserFail $ pure $ Left str

join :: ParserFail (Either String a) -> ParserFail a join (ParserFail p) = ParserFail (Monad.join <$> p)

Btw. I think that 'fail' does not help us. We cannot call it within an Applicative context, because that would require (>>=).