Thursday, July 16, 2009

Safe navigation operators for Scala

Welcome to my first blog post.

I searched the net for a Scala analog to Groovy's safe navigation, but I didn't find anything, so I made my own. It's like a monad but I haven't thought much about the implications of really making it into a real monad so I haven't monadized it at this point.

Here is some example data to demonstrate how you use these safe navigation operators in Scala:
case class Person(var name: String, var bud: Person = null)

val p1 = new Person("bubba")
val p2 = new Person("fred", p1)
val p3 = new Person("mary", p2)

println(opt(p3)?!(_.bud)?!(_.bud)!(_.name.drop(1)))
This code prints "ubba". It uses ?! like Groovy's ?. operator (?. isn't a legal operator in Scala because of the .), but you need the extra parentheses or it doesn't parse like you would want (I think Scala needs the parentheses to disambiguate the scope for the _). The final ! operator returns the closure value instead of wrapping it with an OptionalRef.

If you go one more step, you'll get () as the return value:
println(opt(p3)?!(_.bud)?!(_.bud)?!(_.bud)!(_.name.drop(1)))
That code prints "()". If you don't like the first opt(Any) and you want to do implicit conversions, you can import OptionalRef.Implicit._

So, it's noisier than Groovy, but a lot less noisy than nested "if" expressions.


Here's the code, modified from James Iry's comment:
def notNull[T](x : T) = if (x == null) None else Some(x)
implicit def richOption[T](x : Option[T]) = new {
def ?![B](f : T => B) = notNull(f(x get))
def ![B](f : T => B) = if (x isDefined) f(x get)
}


I like that much better than my old, slightly longer, code :)
object OptionalRef {
object Implicit {
implicit def obj2OptionalRef[T <: Object](input: T): OptionalRef[T] = {
if (input == null) NoRef else Ref(input)
}
}
def opt[T](obj: T) = Ref[T](obj)
abstract class OptionalRef[+T] {
def isEmpty: Boolean
def ?![U](st: (T) => U): OptionalRef[U] = {
(if (isEmpty) NoRef else st(get)) match {
case r: OptionalRef[U] => r
case null => NoRef
case o: U => Ref(o)
}
}
def !(block: (T) => Any): Any = if (!isEmpty) block(get)
def get: T
}
class Ref[+T](x: T) extends OptionalRef[T] {
def isEmpty = false
def get = x
override def toString = "Ref(" + get + ")"
}
object Ref {
def apply[T](obj: T) = new Ref(obj)
}
object NoRef extends OptionalRef[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException("NoRef.value")
override def toString = "NoRef"
}
}

4 comments:

  1. In a private email you asked about a relationship to monads. Sure enough, Scala comes with a monad that behaves like your OptionalRef. It's called Option.

    scala> def notNull[T](x : T) = if (x == null) None else Some(x)
    notNull: [T](T)Option[T]

    scala> val x : String = "hello"
    x: String = hello

    scala> notNull(x) map (_.length)
    res0: Option[Int] = Some(5)

    scala> val y : String = null
    y: String = null

    scala> notNull(y) map (_.length)
    res1: Option[Int] = None

    We can pimp it a bit to have the ?! operator.

    scala> implicit def richOption[T](x : Option[T]) = new {def ?![B](f : T => B) = x map f}
    richOption: [T](Option[T])java.lang.Object{def ?![B]((T) => B): Option[B]}
    scala> notNull(x)?!(_.length)
    res2: Option[Int] = Some(5)

    scala> notNull(y)?!(_.length)
    res3: Option[Int] = None

    ReplyDelete
  2. Actually, I started out by looking at Option (thanks to your blog series on monads http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html), but I couldn't see a way to get Some(x) map f to return a None, which I need in this case. I really like your anonymous class; I hadn't seen that construct before. So the target expression still returns Some(null):

    notNull(p3) ?! (_.bud) ?! (_.bud) ?! (_.bud)

    But we can use your notNull() function to fix that:

    implicit def richOption[T](x : Option[T]) = new {def ?![B](f : T => B) = notNull((x map f) get)}

    I like your solution a lot better than mine!

    ReplyDelete
  3. Actually, I guess this is even simpler:

    implicit def richOption[T](x : Option[T]) = new {def ?![B](f : T => B) = notNull(f(x get))}

    ReplyDelete
  4. If you want `Some(x) map f` to be able to return None, then what you *really* want is flatMap. Option with flatMap (monadic bind) is almost exactly the same as Groovy's safe navigation, but done in such a way that the type system can guarantee safety. Example forthcoming on the next post...

    ReplyDelete