tags:

views:

202

answers:

3

I'm making a small issue management system in Lift to learn both Scala and Lift.

I have a view that displays a single issue belonging to a project. Before I bind data to the view template, I want to check that I have all data required, so I want to specifically check that:

  • A project ID param has been supplied
  • A Project exists with the supplied project ID
  • An issue ID param has been supplied
  • An Issue exists with the supplied issue ID

And these need to be evaluated in order, so if I was to write it now with my current understanding of Scala, I would do the following:

class Project {
    def findByID(xhtml:NodeSeq): NodeSeq = 
        param("projectID") match {
            case Full(projectID) =>
                Project.findByID(projectID) match {
                    case Full(projectID) =>
                        param("issueID") match {
                            ....
                        }
                    case _ => Text("Project does not exist")
                }
            case _ => Text("projectIDNotSupplied")
        }
}

So I'm wondering if there is an easier way to perform this? I think that a for expression might be able to perform something similar. Be aware that Project.findByID returns a Box[Project].

+1  A: 

I don't know Lift, but there are a couple of things I have seen in it's implementation. One of them is the failure methods: ?~ and ?~!. I'm not sure exactly how one might go about using them, but it seems to be useful. Another is open_!, to throw exceptions.

Now, a Box support map, flatMap, filter and foreach, so it can be fully used inside a for comprehension:

for(projectID <- param("projectID");
    if Project.findByID(projectID).isDefined;
    issueID <- param("issueID");
    if Issue.findByID(issueID).isDefined)
yield ...

This won't get you the error messages. For those, I'm guessing the methods I mentioned, or others like ~>, might provide the answer.

Daniel
Thanks Daniel, I was trying to use the for expression in the wrong way to handle this, I don;t fully understand them yet. I'll give this a try when I get home and see if the Box error handling methods return specific info about the failure.
Brian Heylin
+1  A: 

I don't know Lift so I can't answer any Lift-specific questions. I did, however, came up with a way to solve one of your problem which is how to write a sequence of check-then-operate actions without resorting to nested pattern matching.

The main datatype in use here is Option, but I am sure it will be quite easy to adapt to your needs. What we want to accomplish here is to do a sequence of

  1. check condition
  2. continue if successful
  3. terminate and return something otherwise

The code does a kind of short circuit once it encounters a None so the return value is retained when the sequence of actions returns. To use, starts with an Option then write "ifSome" if Option is a Some and "ifNone" if Option is a None, continue until the sequence is finished. If a None is encounter at any point in the sequence, the Option returned from call-by-name parameter of "isNone" is retained and returned when the final "toOption" is called. Use "toOption" to get back the actual Option result.

Check out the example in "main" for some use cases. Good luck!

object Options {

  class RichOption[A](a: Option[A]) {

    def ifSome[B](f: A => Option[B]): RichOption[B] = a match {
      case Some(x) => new RichOption(f(x))
      case None => this.asInstanceOf[RichOption[B]]
    }

    def ifNone[B](f: => Option[B]): RichOption[B] = a match {
      case Some(_) => this.asInstanceOf[RichOption[B]]
      case None => new RichNone(f)
    }

    def toOption[A] = a
  }

  class RichNone[A](a: Option[A]) extends RichOption[A](a) {

    override def ifSome[B](f: A => Option[B]): RichOption[B] = this.asInstanceOf[RichOption[B]]

    override def ifNone[B](f: => Option[B]): RichOption[B] = this.asInstanceOf[RichOption[B]]
  }

  implicit def option2RichOption[A](a: Option[A]): RichOption[A] = new RichOption(a)

  def main(args: Array[String]) {
    println(Some("hello") ifSome(s => Some(s.toUpperCase)) toOption) // prints Some(HELLO)
    println(Some("hello") ifNone(Some("empty")) toOption) // prints Some(hello)
    println(Some("hello") ifSome(s => Some(s.toUpperCase)) ifNone(Some("empty")) toOption) // prints Some(HELLO)
    println(Some("hello") ifNone(Some("empty")) ifSome(s => Some(s.toUpperCase)) toOption) // prints Some(HELLO)
    println((None: Option[String]) ifSome(s => Some(s.toUpperCase)) toOption) // prints None
    println((None: Option[String]) ifNone(Some("empty")) toOption) // prints Some(empty)
    println((None: Option[String]) ifSome(s => Some(s.toUpperCase)) ifNone(Some("empty")) toOption) // prints Some(empty)
    println((None: Option[String]) ifNone(Some("empty")) ifSome(s => Some(s.toUpperCase)) toOption) // prints Some(empty)
    println(Some("hello world") ifSome(s => Some(s.toUpperCase)) ifNone(Some("empty")) ifSome(s => Some(s.length)) ifNone(None) toOption) // prints Some(11)
    println(Some("hello world") ifSome(_ => None) ifNone(Some("goodbye world")) ifSome(s => Some(s.length)) ifNone(None) toOption) // prints Some(goodbye world)
    println((None: Option[String]) ifSome(s => Some(s.toUpperCase)) ifNone(Some("empty")) ifSome(s => Some(s.length)) ifNone(None) toOption) // prints Some(empty)
    println((None: Option[String]) ifSome(_ => None) ifNone(Some("goodbye world")) ifSome(s => Some(s.length)) ifNone(None) toOption) // prints Some(goodbye world)
  }
}
Walter Chang
Thanks Walter, that's an interesting solution I'll give it a try out when I get home, exercise my Scala brain muscles.
Brian Heylin
+4  A: 

Sorry I'm so late to the show, but as Daniel says you can indeed use Lift's Box and ?~ to do something like this. For example:

import net.liftweb.util.Box
import net.liftweb.util.Box._

class Project {
  def findByID(xhtml:NodeSeq): NodeSeq = 
    (for {
      projectID <- param("projectID") ?~ "projectIDNotSupplied"
      project <- Project.findById(projectID) ?~ "Project does not exist"
      issueID <- param("issueID") ?~ "issueIDNotSupplied"
      ...
    } yield {
      ...
    }) match {
      case Full(n) => n
      case Failure(msg, _, _) => Text(msg)
      case _ => Text("fail")
    }
}

What ?~ does is turn an Empty Box into a Failure Box with the given String error message, but does nothing to a Full (success) Box. So the return value of findByID will be Full if everything succeeds, or Failure (with the given error message) otherwise. If you want the Failures to chain, then use ?~! instead.

Jorge Ortiz
Hey Jorge looks like this will work a treat, only that this is a snippet, so as far as I know the return type must be :NodeSeq. If not I get an error saying the method cannot be found. To get around this I can wrap the whole for() in a block and call openOr(Text("fail")) on itBut now when one of the dependencies fail I get the message "fail". I was trying to use the run() method but I'm not quite getting it. any ideas?
Brian Heylin
I've edited the code snippet above to return NodeSeq.
Jorge Ortiz
Excellent, Thanks Jorge. That was one of those "why didn't I think of that cases". Thanks again
Brian Heylin