Question

I'm new to Scala and trying to understand the syntax the pattern matching constructs, specifically from examples in Unfiltered (http://unfiltered.databinder.net/Try+Unfiltered.html).

Here's a simple HTTP server that echos back Hello World! and 2 parts of the path if the path is 2 parts long:

package com.hello

import unfiltered.request.GET
import unfiltered.request.Path
import unfiltered.request.Seg
import unfiltered.response.ResponseString

object HelloWorld {
  val sayhello = unfiltered.netty.cycle.Planify {
    case GET(Path(Seg(p :: q :: Nil))) => {
      ResponseString("Hello World! " + p + " " + q);
    }
  };

  def main(args: Array[String]) {
    unfiltered.netty.Http(10000).plan(sayhello).run();
  }
}

Also for reference the source code for the Path, Seg, and GET/Method objects:

package unfiltered.request

object Path {
  def unapply[T](req: HttpRequest[T]) = Some(req.uri.split('?')(0))
  def apply[T](req: HttpRequest[T]) = req.uri.split('?')(0)
}

object Seg {
  def unapply(path: String): Option[List[String]] = path.split("/").toList match {
    case "" :: rest => Some(rest) // skip a leading slash
    case all => Some(all)
  }
}

class Method(method: String) {
  def unapply[T](req: HttpRequest[T]) = 
    if (req.method.equalsIgnoreCase(method)) Some(req)
    else None
}

object GET extends Method("GET")

I was able to break down how most of it works, but this line leaves me baffled:

case GET(Path(Seg(p :: q :: Nil))) => {

I understand the purpose of the code, but not how it gets applied. I'm very interested in learning the ins and outs of Scala rather than simply implementing an HTTP server with it, so I've been digging into this for a couple hours. I understand that it has something to do with extractors and the unapply method on the GET, Path, and Seg objects, I also knows that when I debug it hits unapply in GET before Path and Path before Seg.

I don't understand the following things:

  1. Why can't I write GET.unapply(req), but I can write GET(req) or GET() and it will match any HTTP GET?

  2. Why or how does the compiler know what values get passed to each extractor's unapply method? It seems that it will just chain them together unless one of them returns a None instead of an Some?

  3. How does it bind the variables p and q? It knows they are Strings, it must infer that from the return type of Seg.unapply, but I don't understand the mechanism that assigns p the value of the first part of the list and q the value of the second part of the list.

  4. Is there a way to rewrite it to make it more clear what's happening? When I first looked at this example, I was confused by the line val sayhello = unfiltered.netty.cycle.Planify {, I dug around and rewrote it and found out that it was implicitly creating a PartialFunction and passing it to Planify.apply.

Was it helpful?

Solution

One way to understand it is to rewrite this expression the way that it gets rewritten by the Scala compiler.

unfiltered.netty.cycle.Planify expects a PartialFunction[HttpRequest[ReceivedMessage], ResponseFunction[NHttpResponse]], that is, a function that may or may not match the argument. If there's no match in either of the case statements, the request gets ignored. If there is a match -- which also has to pass all of the extractors -- the response will be returned.

Each case statement gets an instance of HttpRequest[ReceivedMessage]. Then, it applies it with left associativity through a series of unapply methods for each of the matchers:

// The request passed to us is HttpRequest[ReceivedMessage]
// GET.unapply only returns Some if the method is GET
GET.unapply(request) flatMap { getRequest =>
    // this separates the path from the query
    Path.unapply(getRequest) flatMap { path =>
        // splits the path by "/"
        Seg.unapply(path) flatMap { listOfParams =>
            // Calls to unapply don't end here - now we build an
            // instance of :: class, which 
            // since a :: b is the same as ::(a, b)
            ::.unapply(::(listOfParams.head, listOfParams.tail)) flatMap { case (p, restOfP) =>
                ::.unapply(::(restOfP.head, Nil)) map { case (q, _) =>
                    ResponseString("Hello World! " + p + " " + q)
                }
            }
        }
    }
}

Hopefully, this gives you an idea of how the matching works behind the scenes. I'm not entirely sure if I got the :: bit right - comments are welcome.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top