Search code examples
parsinghaskellparsecparser-combinators

Haskell ReadP parse at least one of the list


I try to parse a command (create) with the ReadP standard lib. My command should start by the string create, then contains at least one word/tag/due, and potentially an option. Here my actual expression:

createExpr :: ReadP [Arg]
createExpr = do
  skipSpaces
  cmd <- SetCmd <$> cmdAliasExpr ["create", "add"]
  skipSpaces
  rest <-
    many1
    $   (AddWord <$> wordExpr)
    <|> (AddTag <$> addTagExpr)
    <|> (SetDue <$> dueExpr)
    <|> (AddOpt <$> optExpr)
  skipSpaces
  return $ cmd : rest

The problem is, if I call create with just one option, it parses well. But it shouldn't, since I expect at least one word/tag/due. How can I express this?


[EDIT] I found the solution, thanks to @M. Aroosi.

In fact, I used the wrong operator. <++, the local, exclusive, left-biased choice, fits better my needs. Once an expression is matched, it should not check for the other ones:

notAnOpt arg = case arg of
  AddOpt _ -> False
  _        -> True


createExpr :: ReadP [Arg]
createExpr = do
  skipSpaces
  cmd <- SetCmd <$> cmdAliasExpr ["create", "add"]
  skipSpaces
  rest <-
    many1
    $   (AddTag <$> addTagExpr)
    <++ (SetDue <$> dueExpr)
    <++ (AddOpt <$> optExpr)
    <++ (AddWord <$> wordExpr)
  skipSpaces
  guard $ isJust $ find notAnOpt rest
  return $ cmd : rest

Solution

  • A simple solution would be to use guard from Control.Monad.
    Assuming a function like isOpt :: Arg -> Bool which is along the lines of

    isOpt :: Arg -> Bool
    isOpt (AddOpt _) = True
    isOpt _          = False 
    

    then your definition of createExpr changes to

    createExpr :: ReadP [Arg]
    createExpr = do
        skipSpaces
        cmd <- SetCmd <$> cmdAliasExpr ["create", "add"]
        skipSpaces
        rest <-
          many1
          $   (AddWord <$> wordExpr)
          <|> (AddTag <$> addTagExpr)
          <|> (SetDue <$> dueExpr)
          <|> (AddOpt <$> optExpr)
        guard $ at_least_one_non_optional rest
        skipSpaces
        return $ cmd : rest
      where at_least_one_non_optional = not . null . filter (not . isOpt)
    

    guard basically fails the parser when its argument is False, and more generally it works with any Alternative by returning empty when the argument is False.