目次

Version 1, last updated by Debilski at Nov 28 02:17 UTC

Memoizing Requests in a DispatchPF

This page deals with the problem that the isDefinedAt part of a PartialFunction is called twice in some Lift contexts. First when a PartialFunction with the correct domain is searched. And then again before it is actually executed. Even though we’re mainly speaking of DispatchPF, this also holds for other partial functions in Lift.

Suppose, you wanted to give your users the opportunity to download a PDF document. You’re easily done with a DispatchPF, which you register with LiftRules.dispatch(servePdf).

def servePdf: LiftRules.DispatchPF = { 
  case Req("pdf" :: docId :: Nil, _, GetRequest) => () => {
    val pdfDoc: Box[PdfDoc] = createFromId(docId)
    ...
    pdfDoc match {
      case Full(pdf) => Full(StreamingResponse(...))
      case _ => Empty
    }
  }
}

Now, whatever the docId is, the DispatchPF will match and try to retrieve and return some document. If for some reasons, the PDF could not be found or generated you’re stuck in there and Empty will be returned.
This will be all right in many situations. You could display a 404 page and be done.

But maybe you don’t want to fail in the very first DispatchPF. Maybe you want to display some other information in that case or maybe you’ve simply overloaded your URI namespace or maybe you just want to be sure that really no page matched the request before returning the 404.

Not getting stuck in the very first DispatchPF

One easy way to work around this, is of course using an if-guard.

case Req("pdf" :: docId :: Nil, _, GetRequest) if createFromId(docId).isDefined => () => {
  val pdfDoc: Box[PdfDoc] = createFromId(docId) // what? again?
  ...
}

If createFromId(docId).isDefined == false, Lift will simply move on to the next DispatchPF and ask for a respose.

However, there is a small problem with this solution: You might not know if you actually can create the PDF without trying it; and you also can’t assign the result of createFromId to any variable in order to use it.

So, unfortunately this is impossible (nor does it make much sense):

case Req("pdf" :: docId :: Nil, _, GetRequest) if Some(pdfDoc@createFromId(docId)) => ... // use pdfDoc here

Extractors

Luckily, you can use Scala’s powerful extractors to help you with some advanced pattern matching

object CreateFromId {
  def unapply(id: String): Option[PdfDoc] = createFromId(id)
}
def servePdf: LiftRules.DispatchPF = { 
  case Req("pdf" :: CreateFromId(pdfDoc) :: Nil, _, GetRequest) => () => {
    // use pdfDoc
}}

That’s great: you return early from the isDefinedAt phase and you still have pdfDoc in scope when you enter the body of the partial function. But still, there is one technical issue left.

After it has been decided that a PDF can be served and Lift tries to execute the DispatchPF and stream your file, something unexpected happens: The partial function is checked again! Yes, that’s true. Scala will check again if the PartialFunction isDefinedAt our query and only then will it really call its body.

Memoization

Again, we’re lucky to use Lift because we can easily memoize the result during the first check and then use the memoized result for the second check. It is easily done; all we have to do is extend our CreateFromId object a little.

private object unapplyMemo extends RequestMemoize[Any, Option[Profile]] {
  override protected def __nameSalt = Helpers.randomString(20)
}
object FindFromId {
  def unapply(id: String): Option[Profile] = {
    if (S.inStatefulScope_?) unapplyMemo(id, createFromId(id))
    else createFromId(id)
  }
}

Of course, you could also use the RequestMemoize object in your original createFromId method and then even take the if-guard from the second paragraph. But using the extractor still saves you one call to the memoizer and spares you from having to deal with the boxed value from createFromId.