Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Signaling failure via the function application parser combinator

I need to do more complex syntax checking on a parser match than the standard notation permits and am currently doing it in the function application ^^. A sample, simplified scenario is checking for duplicate keywords:

def keywords: Parser[List[String]] = "[" ~ repsep(keyword, ",") ~ "]" ^^ {
  case _ ~ ks ~ _ =>
    ks.groupBy(x => x).filter(_._2.length > 1).keys.toList match {
      case Nil => ks
      case x => throw new DuplicateKeywordsException(x)
    }
}

This works, as in my parser will throw an exception, but I want the failure to be captured as a ParseResult.Failure capturing the Input of where it happened. I can't figure out how to signal this from within a ^^ block or use some other construct to achieve the same end.

like image 997
Arne Claassen Avatar asked Oct 20 '22 20:10

Arne Claassen


1 Answers

Ok, I followed Erik Meijer's advice of following the Types down the happy path. Looking at how ^^ is defined in Programming in Scala (which differs from the actual code), i realized it was basically just a Map function:

def ˆˆ [U](f: T => U): Parser[U] = new Parser[U] {
  def apply(in: Input) = p(in) match {
    case Success(x, in1) => Success(f(x), in1)
    case failure => failure
  }
}

Basically it's Parser[T] => Parser[U].

Parser[T] itself is a function of Input => ParseResult[T] and ^^ just defines a new parser by supplying the apply method, which on invocation either transforms Success[T] to Success[U] or just passes along Failure.

To achieve the goal of injecting a new Failure during mapping I need a new mapping function that takes a function like f: T => Either[String,U] so I can signal an error message or successful mapping. I chose Either with string, since Failure just takes a string message. This new mapping function is then added to Parser[U] via an implicit class:

implicit class RichParser[+T](p: Parser[T]) {
  def ^^? [U](f: T => Either[String,U]): Parser[U] = new Parser[U] {
    def apply(in: Input) = p(in) match {
      case Success(x, in1) => f(x) match {
        case Left(error) => Failure(error,in1)
        case Right(x1) => Success(x1,in1)
      }
      case failure:Failure => failure
      case error:Error => error
    }
  }
}

And now keywords can be defined as:

def keywords: Parser[List[String]] = "[" ~ repsep(keyword, ",") ~ "]" ^^? {
  case _ ~ ks ~ _ =>
    ks.groupBy(x => x).filter(_._2.length > 1).keys.toList match {
      case Nil => Right(ks)
      case x => Left("found duplicate keywords: "+x.reduce[String] { case (a, b) => s"$a, $b"})
    }
}
like image 171
Arne Claassen Avatar answered Oct 22 '22 20:10

Arne Claassen