views:

172

answers:

4

For lift development, I sometimes need to use matchcase statements like the following. (Rewritten to plain scala for easier understanding.) One note to them: These are actually different partial functions, defined in different parts of the code, so it’s important that a case statement fails in or before the guard in order to have the other partial functions evaluated (if matching fails, that is).

// The incoming request
case class Req(path: List[String], requestType: Int)

// Does some heavy database action (not shown here)
def findInDb(req: Req):Option[Int] = 
  if(req.path.length > 3) Some(2) else None

Req("a"::"b"::Nil, 3) match {
  case r@Req(`path` :: _ :: Nil, 3) if findInDb(r).isDefined =>
    doSomethingWith(findInDb(r))
  case r@Req(`path` :: _ :: Nil, _) => doDefault
  case _ => doNothing
}

Now, in order to know that the case statement succeeds, I must query the database with findInDb and check whether the result is valid. Afterwards, I must call it again to use the value.

Doing something like

case r@Req(path, 3) if {val res = findInDb(r); res.isDefined} =>

does not work because the scope of res is then limited to inside the braces.

I can of course define a var res = _ outside and assign to it, but I don’t feel well doing this.

Is it by any means possible to declare a variable inside the guard? If it is possible to do case r@Req(…) why not case r@Req() if res@(r.isDefined)?

A: 

Did you try case r @ Req() if res@(r.isDefined)?

scala> val t3 =(1, "one", 1.0)
t3: (Int, java.lang.String, Double) = (1,one,1.0)

scala> t3 match { case t @ (1, s, d) if t._3 < 2.0 => println("OK"); case _ => println("No-Go") }
OK
Randall Schulz
I don’t quite understand your answer. As I’ve written (or at least had the hope that people would believe that I actually tried it), I tried using `if res@(something)` and it is not valid syntax.
Debilski
+7  A: 

You're actually very close. The key piece missing is to use an extractor rather than a guard expression.

object FindInDb{
   def unapply(r:Req):Option[Int]= findInDb(r)    
}

Req("a"::"b"::Nil, 3) match {
  case dbResult@FindInDb(Req(`path` :: _ :: Nil, 3))=> doSomethingWith(dbResult)
  case Req(`path` :: _ :: Nil, _) => doDefault
  case _ => doNothing
}

Extractors aren't actually required to return only information already present in their arguments, that's just the common use case. You can actually use any partial function, lift it to Option, and be able to match on information about both whether the function is defined and its value.

Dave Griffith
Hmm. Interesting approach. I knew the technique but did not believe I could use it in that case. I’ll think about whether it’s useful in my case.
Debilski
It get’s pretty messed up, of course, when I want to use not only `req` but also `id` as in `case req@Req(```path``` :: id :: Nil, _ ,_)`… Let’s just hope this won’t happen.
Debilski
Should be fine, actually. No reason you can't use @-patterns or bind new variables inside the args to an extractor.
Dave Griffith
Nevermind, this is, upon further reflection, exactly backward. This correct pattern above would have to be just case FindInDb(dbResult)=> doSomethingWith(dbResult)which would obviously make it more difficult to use other intermediate matches, and require moving the patter matching logic into the extractor. Bad idea.
Dave Griffith
+1  A: 

What's wrong with refactoring the if expression to be inside the case statement?

Req("a"::"b"::Nil, 3) match {
  case r@Req(`path` :: _ :: Nil, 3) =>
    val res=findInDb(r)
    if(res.isDefined) doSomethingWith(res)
    else doDefault
  case r@Req(`path` :: _ :: Nil, _) => doDefault
  case _ => doNothing
}
Ken Bloom
Because then it won’t try to match the other statements anymore. And I can’t refactor it, because these are partial functions and somewhere else in the code. So, everything has to be done in the matching statement.
Debilski
+1  A: 

You could create a little bit of infrastructure to let you wrap a var in a less exposed way:

class Memory[M] {
  // Could throw exceptions or whatnot if you tried to assign twice
  private[this] var mem: Option[M] = None
  def apply(om: Option[M]) = { mem = om; mem }
  def apply(m: M) = { mem = Some(m); mem }
  def apply() = { mem }
}

// Need to create an object that memorizes the correct type
object MemorizeInt {
  def unapply[A](a: A) = Some((a,new Memory[Int]))
}

case class Req(path: List[String], requestType: Int)
def findInDb(req: Req) =  if(req.path.length > 0) Some(2) else None
def doSomethingWith(oi: Option[Int]) {
  println(oi)
}

Req("a"::"b"::Nil, 3) match {
  case MemorizeInt(r@Req(path :: _ :: Nil, 3),m) if m(findInDb(r)).isDefined =>
    doSomethingWith(m())
  case r@Req(path :: _ :: Nil, _) => {}
  case _ => {}
}

Alternatively, you can move the work from after the => into the conditional by using map:

case class Req(path: List[String], requestType: Int)
def findInDb(req: Req) =  if(req.path.length > 0) Some(2) else None
def doSomethingWith(i: Int) { println(i) }

Req("a"::"b"::Nil, 3) match {
  case r@Req(path :: _ :: Nil, 3) if 
    findInDb(r).map(m => { doSomethingWith(m); m }).isDefined => {}
  case r@Req(path :: _ :: Nil, _) => println("default")
  case _ => println("messed up")
}
Rex Kerr