Вопрос

Triggered by another question (which has been subsequently edited away though), I wanted to try out how easy it would be to chain calls to Scala 2.10's Try construct (cf. this presentation), using for-comprehensions.

The idea is to have a list of tokens and match them against a sequence of patterns, then return the first error or the successfully matched pattern. I arrived at the following pretty awkward version, and I wonder if this can be made simpler and nicer:

import util.Try

trait Token
case class Ident  (s: String) extends Token
case class Keyword(s: String) extends Token
case class Punct  (s: String) extends Token
case object NoToken extends Token
case class FunctionDef(id: Ident)

case class Expect[A](expectation: String)(pattern: PartialFunction[Token, A]) {
  def unapply(tup: (Try[_], Token)) = Some(tup._1.map { _ => 
     pattern.lift(tup._2).getOrElse(throw new Exception(expectation))
  })
}

Now construct the expectations for Keyword("void") :: Ident(id) :: Punct("(") :: Punct(")") :: tail

val hasVoid   = Expect("function def starts with void") { case Keyword("void") => }
val hasIdent  = Expect("expected name of the function") { case id: Ident       => id }
val hasOpen   = Expect("expected opening parenthesis" ) { case Punct("(")      => }
val hasClosed = Expect("expected closing parenthesis" ) { case Punct(")")      => }

Construct a full test case:

def test(tokens: List[Token]) = {
  val iter = tokens.iterator
  def next(p: Try[_]) = Some(p -> (if (iter.hasNext) iter.next else NoToken))
  def first() = next(Try())

  val sq = for {
    hasVoid  (vd) <- first()
    hasIdent (id) <- next(vd)
    hasOpen  (op) <- next(id)
    hasClosed(cl) <- next(op)
  } yield cl.flatMap(_ => id).map(FunctionDef(_))

  sq.head
}

The following verifies the test mehod:

// the following fail with successive errors
test(Nil)
test(Keyword("hallo") :: Nil)
test(Keyword("void" ) :: Nil)
test(Keyword("void" ) :: Ident("name") :: Nil)
test(Keyword("void" ) :: Ident("name") :: Punct("(") :: Nil)
// this completes
test(Keyword("void" ) :: Ident("name") :: Punct("(") :: Punct(")") :: Nil)

Now especially the additional flatMap and map in yield seems horrible, as well as the need to call head on the result of the for comprehension.

Any ideas? Is Try very badly suited for for comprehensions? Shouldn't either Either or Try be "fixed" to allow for this type of threading (e.g. allow Try as a direct result type of unapply)?

Это было полезно?

Решение

The trick seems to be to not create Try instances in the inner structure, but instead let that throw exceptions and construct one outer Try.

First, let's get rid of the Try[Unit]'s:

case class Expect(expectation: String)(pattern: PartialFunction[Token, Unit]) {
  def unapply(token: Token) = 
    pattern.isDefinedAt(token) || (throw new Exception(expectation))
}

case class Extract[A](expectation: String)(pattern: PartialFunction[Token, A]) {
  def unapply(token: Token) = Some(
    pattern.lift(token).getOrElse(throw new Exception(expectation))
  )
}

Then the checks become:

val hasVoid   = Expect ("function def starts with void") { case Keyword("void") => }
val getIdent  = Extract("expected name of the function") { case id: Ident       => id }
val hasOpen   = Expect ("expected opening parenthesis" ) { case Punct("(")      => }
val hasClosed = Expect ("expected closing parenthesis" ) { case Punct(")")      => }

And the test method:

def test(tokens: List[Token]) = Try {
  val iter = tokens.iterator
  def next() = Some(if (iter.hasNext) iter.next else NoToken)

  (for {
    hasVoid()    <- next()
    getIdent(id) <- next()
    hasOpen()    <- next()
    hasClosed()  <- next()
  } yield FunctionDef(id)).head  // can we get rid of the `head`?
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top