8

Is possible to create a haskell expression, using the methods in optparse-applicative, that parses program options like this?

program [-a [-b]] ...

-a and -b are optionals flags (implemented using switch), with the constraint that the -b option only is valid if -a is typed before.

Thanks

fho
  • 6,787
  • 26
  • 71

2 Answers2

11

This is possible, with slight tweaks, two different ways:

  1. You can make a parser that only allows -b if you've got -a, but you can't insist then that the -a comes first, since optparse-applicative's <*> combinator doesn't specify an order.
  2. You can insist that the -b option follows the a option, but you do this by implementing a as a command, so you lose the - in front of it.

Applicative is definitely strong enough for this, since there's no need to inspect the values returned by the parsers to determine whether -b is allowed, so >>= is not necessary; If -a succeeds with any output, -b is allowed.

Examples

I'll use a data type to represent which arguments are present, but in reality these would be more meaningful.

import Options.Applicative

data A = A (Maybe B)   deriving Show 
data B = B             deriving Show

So the options to our program maybe contain an A, which might have a B, and always have a string.

boption :: Parser (Maybe B)
boption = flag Nothing (Just B) (short 'b')

Way 1: standard combinators - -b can only come with -a (any order)

I'll use flag' () (short 'a') which just insists that -a is there, but then use *> instead of <*> to ignore the return value () and just return whatever the boption parser returns, giving options -a [-b]. I'll then tag that with A :: Maybe B -> A and finally I'll make the whole thing optional, so you have options [-a [-b]]

aoption :: Parser (Maybe A)
aoption = optional $ A <$> (flag' () (short 'a' ) *> boption)

main = execParser (info (helper <*> aoption) 
                        (fullDesc <> progDesc "-b is only valid with -a")) 
        >>= print

Notice that since <*> allows any order, we can put -a after -b (which isn't quite what you asked for, but works OK and makes sense for some applications).

ghci> :main -a 
Just (A Nothing)
ghci> :main -a -b
Just (A (Just B))
ghci> :main -b -a
Just (A (Just B))
ghci> :main -b
Usage: <interactive> [-a] [-b]
  -b is only valid with -a
*** Exception: ExitFailure 1

Way 2: command subparser - -b can only follow a

You can use command to make a subparser which is only valid when the command string is present. You can use it to handle arguments like cabal does, so that cabal install and cabal update have completely different options. Since command takes a ParserInfo argument, any parser you can give to execParser can be used, so you can actually nest commands arbitrarily deeply. Sadly, commands can't start with -, so it'll be program [a [-b]] ... instead of program [-a [-b]] ....

acommand :: Parser A
acommand = subparser $ command "a" (info (A <$> (helper <*> boption)) 
                                         (progDesc "you can '-b' if you like with 'a'"))

main = execParser (info (helper <*> optional acommand) fullDesc) >>= print

Which runs like this:

ghci> :main 
Nothing
ghci> :main a 
Just (A Nothing)
ghci> :main a -b
Just (A (Just B))
ghci> :main -b a
Usage: <interactive> [COMMAND]
*** Exception: ExitFailure 1

So you have to precede -b with a.

AndrewC
  • 32,300
  • 7
  • 79
  • 115
4

I'm afraid you can't. This is precisely the scenario that Applicative alone can't handle while Monad can: changing the structure of later actions based on earlier results. In an applicative computation, the "shape" always needs to be known beforehand; this has some advantages (like speeding up so array combinations, or giving out a nice readable help screen for command-line options), but here it limits you to parsing "flat" options.

The interface of optparse-applicative also has Alternative though, which does allow dependent parsing, albeit in a different way as shown by AndrewC.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Are you sure? We _do_ know this structure beforehand, it's just nested. Couldn't you use `command` on __a__ and use the subparser to implement __b__? – AndrewC Aug 10 '14 at 00:18
  • It's fixed, but not Applicative. You could include some static branching behavior by adding a type like `f Bool -> (Bool -> f r) -> f r`, i.e. a type restrained `(>>=)`. – J. Abrahamson Aug 10 '14 at 01:01
  • 1
    Sorry, @leftaroundabout, but I cannot agree with your reasoning. Applicative is strong enough for this kind of thing, otherwise we wouldn't be able to use the applicative interface to parsec to allow = to only appear after an identifier. You don't need to check the text of the identifier to know that = can follow it, so you don't need bind. Similarly here, you can parse -a and return (), (not the letter a). You don't need to inspect the value () to know that -b is now allowed, so you don't need bind. The problem is purely that in this context, <*> is non-deterministic about order. – AndrewC Aug 11 '14 at 08:08
  • @AndrewC: the problem is that "implemented using `switch`" does imply that we need to "inspect" the boolean value to know whether `-b` will be allowed; that's what applicatives can't do which is the point of my answer. You can certainly get a solution with other tricks, as shown in your answer, but you're not really using the applicative interface then but built-in higher order functions. The only straightforward and all-purpose solution would be a monad (or at least arrow) interface. – leftaroundabout Aug 11 '14 at 11:28
  • 1
    @leftaroundabout I'm using completely standard Applicative & Alternative combinators in my first method, just on top of a non-optional flag' (`optional` is from Alternative). I took "implemented using switch" to mean "currently implemented using switch". Can we agree to "I'm afraid you can't if you stick to using `switch`"? The reason I'm concerned is that as currently written it implies that Applicative is an extremely weak interface for parsing, which it isn't. – AndrewC Aug 11 '14 at 12:23
  • `Applicative` _is_ an extremely weak interface. But yeah, `Alternative` is reasonably powerful; frankly I overlooked that instance. – leftaroundabout Aug 11 '14 at 12:50