Reputation: 13585
I would like to know what the best Scala imitation of Groovy's safe-dereference operator (?.), or at least some close alternatives are?
I've discussed it breifly on Daniel Spiewak's blog, but would like to open it up to StackOverFlow...
For the sake of everyone's time, here is Daniel's initial response, my counter, and his 2nd response:
@Antony
Actually, I looked at doing that one first. Or rather, I was trying to replicate Ragenwald’s andand “operator” from Ruby land. The problem is, this is a bit difficult to do without proxies. Consider the following expression (using Ruby’s andand, but it’s the same with Groovy’s operator):
test.andand().doSomething()
I could create an implicit conversion from Any => some type implementing the andand() method, but that’s where the magic stops. Regardless of whether the value is null or not, the doSomething() method will still execute. Since it has to execute on some target in a type-safe manner, that would require the implementation of a bytecode proxy, which would be flaky and weird (problems with annotations, final methods, constructors, etc).
A better alternative is to go back to the source of inspiration for both andand as well as Groovy’s safe dereference operator: the monadic map operation. The following is some Scala syntax which uses Option to implement the pattern:
val something: Option[String] = … // presumably could be either Some(…) or None
val length = something.map(_.length)
After this,
length
either be Some(str.length) (where str is the String object contained within the Option), or None. This is exactly how the safe-dereferencing operator works, except it uses null rather than a type-safe monad.As pointed out above, we could define an implicit conversion from some type T => Option[T] and then map in that fashion, but some types already have map defined, so it wouldn’t be very useful. Alternatively, I could implement something similar to map but with a separate name, but any way it is implemented, it will rely upon a higher-order function rather than a simple chained call. It seems to be just the nature of statically typed languages (if anyone has a way around this, feel free to correct me).
Daniel Spiewak Monday, July 7, 2008 at 1:42 pm
My 2nd question:
Thanks for the response Daniel regarding ?. I think I missed it! I think I understand what you’re proposing, but what about something like this, assuming you don’t have control over the sources:
company?.getContactPerson?.getContactDetails?.getAddress?.getCity
Say it’s a java bean and you can’t go in and change the return values to Something[T] - what can we do there?
Antony Stubbs Tuesday, July 21, 2009 at 8:07 pm oh gosh - ok on re-read that’s where you’re proposing the implicit conversion from T to Option[T] right? But would you still be able to chain it together like that? You’d still need the map right? hmm….
var city = company.map(_.getContactPerson.map(_.getContactDetails.map(_.getAddress.map(_.getCity))))
?
Antony Stubbs Tuesday, July 21, 2009 at 8:10 pm
His 2nd response:
@Antony
We can’t really do much of anything in the case of company?.getContactPerson, etc… Even assuming this were valid Scala syntax, we would still need some way to prevent the later calls in the chain. This is not possible if we’re not using function values. Thus, something like map is really the only option.
An implicit conversion to Option wouldn’t be bad, but by making things implicit, we’re circumventing some of the protection of the type system. The best way to do this sort of thing is to use for-comprehensions in concert with Option. We can do map and flatMap, but it’s much nicer with magical syntax:
for {
c < - company
person <- c.getContactPerson
details <- person.getContactDetails
address <- details.getAddress
} yield address.getCity
Daniel Spiewak Tuesday, July 21, 2009 at 9:28 pm
P.s. if Daniel posts his original answers on his blog as answers, I will edit the question to remove them for the sake of the System.
Upvotes: 25
Views: 7776
Reputation: 1096
I created a macro-based library to solve exactly this problem:
https://github.com/ryanstull/ScalaNullSafe
The way it works is it decomposes an expression that you pass into it and rewrites it adding in null-checks at each point that there could be a null
value.
For example:
import com.ryanstull.nullsafe._
case class A(b: B)
case class B(c: C)
case class C(d: D)
case class D(e: E)
case class E(s: String)
val a = A(B(C(null)))
?(a.b.c.d.e.s) //No NPE! Just returns null
val a2 = A(B(C(D(E("Hello")))))
?(a2.b.c.d.e.s) //Returns "Hello"
What is happening behind the scenes is that ?(a.b.c)
gets transformed into
if(a != null){
val b = a.b
if(b != null){
b.c
} else null
} else null
There are other variants as well such as:
opt: A -> Option[A]
notNull: Boolean
And ??
which acts as a null-coalescing operator.
The project README has more details.
Upvotes: 0
Reputation: 5974
I liked Daniel C. Sobral's use of for comprehensions--- it gets to the point more quickly than the cascade of nested match
es I had been doing. However, it's still not very convenient because there are still intermediate dummy variables (and too much typing).
We want something like a?.b?.c?.d
so we don't have to think about what comes in between: just try to get something and give me an Option
in case you can't get it.
For context, suppose I have
case class Inner(z: Option[Int])
case class Outer(y: Option[Inner])
val x = Some(Outer(Some(Inner(Some(123)))))
that I want to unpack. The for comprehension would go like the following
for (tmp1 <- x; tmp2 <- tmp1.y; tmp3 <- tmp2.z) yield tmp3
which results in Some(123)
. The problem is too many temporary variables (and the fact that it's partially reading backward).
I find it easier to do it with flatMap
, like this
x.flatMap(_.y.flatMap(_.z))
or
x flatMap {_.y flatMap {_.z}}
which also results in Some(123)
.
One could cut down on the verbosity and use the desired ?
symbol by effectively giving the Option
type a method ?
that does the same thing as flatMap
. Option
is sealed from subclassing, but we can simulate the new method with implicit conversions.
case class OptionWrapper[A](opt: Option[A]) {
def ?[B](f: (A) => Option[B]): Option[B] = opt.flatMap(f)
}
implicit def toOptionWrapper[T](opt: Option[T]) = OptionWrapper(opt)
implicit def fromOptionWrapper[T](wrap: OptionWrapper[T]) = wrap.opt
And then
x ? {_.y ? {_.z}}
yields Some(123
. It's still not perfect because there are nested brackets and underscores that you have to get right, but it's better than any alternatives I've seen.
Upvotes: 0
Reputation: 26738
Create this implicit conversion.
class SafeDereference[A](obj: A) {
def ?[B >: Null](function: A => B): B = if (obj == null) null else function(obj)
}
implicit def safeDereference[A](obj: A) = new SafeDereference(obj)
The usage isn't as pretty as Groovy, but it's not awful.
case class Address(state: String)
case class Person(first: String, last: String, address: Address)
val me = Person("Craig", "Motlin", null)
scala> me ? (_.first)
res1: String = Craig
scala> me ? (_.address)
res2: Address = null
scala> me ? (_.address) ? (_.state)
res3: String = null
Upvotes: 11
Reputation: 41
To follow up Daniel C. Sobral's answer, the reason Option is preferred is because idiomatic Scala does not use null pointers. If you can, rewrite the code to return Options instead of nullable references. Chained flatMaps are cleaner than for-comprehensions, since you don't need a new variable name for each step. If all the values are optional (as in the Groovy example), the Scala approach would look like this:
(company flatMap _.getContactPerson
flatMap _.getContactDetails
flatMap _.getAddress
flatMap _.getCity) match {
case Some(city) => ...
case None => ...
}
If you must use nullable values for Java interoperability, here's an approach that gives you safety without NPE-wrangling or too much clutter:
sealed trait Nullable[+A] {
def apply[B](f:A=>B): Nullable[B]
}
def ?[A](a: A) = a match {
case null => NullRef
case _ => Ref(a)
}
case class Ref[A](value: A) extends Nullable[A] {
def apply[B](f:A=>B) = ?(f(value))
}
object NullRef extends Nullable[Nothing] {
def apply[B](f: Nothing=>B): Nullable[B] = NullRef
}
?(company)(_.getContactPerson)(_.getContactDetails)(_.getAddress)(_.getCity) match {
case Ref(city) => ...
case _ => ...
}
This should be easy to expand to a full Option-style monad if desired.
Upvotes: 4
Reputation: 4706
Not mine but a coworker's
class NullCoalescer[T <: AnyRef](target: T) {
def ?? (other: T) =
if(target == null) other else target
}
object NullCoalescerConversions {
implicit def toNullCoalescer[T <: AnyRef](target: T): NullCoalescer[T] =
new NullCoalescer(target)
}
println (System.getProperty("maybe") ?? "definitely")
Upvotes: 4
Reputation: 13585
Because this would look terrible as a comment, here's a commented version of Walter's code:
/**
* Safe dereference operator. E.g. ?(a.b.c.null.dd)
*/
def ?[A](block: => A) = {
try { block } catch {
// checks to see if the 3rd to last method called in the stack, is the ?() function,
// which means the null pointer exception was actually due to a null object,
// otherwise the ?() function would be further down the stack.
case e: NullPointerException if e.getStackTrace()(2).getMethodName == "$qmark" => {null}
// for any other NullPointerException, or otherwise, re-throw the exception.
case e => throw e
}
And the specification, which passes:
case class Company(employee:Employee)
case class Employee(address:Address){
def lookupAddressFromDb:Address = throw new NullPointerException("db error")
}
case class Address(city:String)
"NullSafe operater" should {
"return the leaf value when working with non-null tree" in {
val company = Company(Employee(Address("Auckland")))
val result = ?( company.employee.address.city )
result mustEq "Auckland"
}
"return null when working with a null element at some point in the tree" in {
val company = Company(null)
val result = ?( company.employee.address.city )
result must beNull
}
"re-throw the NPE when working with a method which actually throws a NullPointerException" in {
val company = Company(Employee(Address("Auckland")))
?( company.employee.lookupAddressFromDb.city ) aka "the null-safe lookup method" must throwA[NullPointerException]
}
}
Upvotes: 1
Reputation: 297265
There are two things that need to be considered here.
First, there is the problem of the "nothing". How do you chain things when a part of the chain may not return anything? The answer is using Option
and for
comprehensions. For example:
scala> case class Address(city: Option[String] = None, street: Option[String] = None, number: Option[Int] = None)
defined class Address
scala> case class Contact(name: String, phone: Option[String] = None, address: Option[Address] = None)
defined class Contact
scala> case class ContactDetails(phone: Option[String] = None, address: Option[Address] = None)
defined class ContactDetails
scala> case class Contact(phone: Option[String] = None, address: Option[Address] = None)
defined class Contact
scala> case class Person(name: String, contactDetails: Option[Contact] = None)
defined class Person
scala> case class Company(name: String, contactPerson: Option[Person] = None)
defined class Company
scala> val p1 = Company("ABC", Some(Person("Dean", Some(Contact(None, Some(Address(city = Some("New England"))))))))
p1: Company = Company(ABC,Some(Person(Dean,Some(Contact(None,Some(Address(Some(New England),None,None)))))))
scala> val p2 = Company("Finnicky", Some(Person("Gimli", None)))
p2: Company = Company(Finnicky,Some(Person(Gimli,None)))
scala> for(company <- List(p1, p2);
| contactPerson <- company.contactPerson;
| contactDetails <- contactPerson.contactDetails;
| address <- contactDetails.address;
| city <- address.city) yield city
res28: List[String] = List(New England)
This is how you are supposed to write code which may return something or not in Scala.
The second problem, of course, is that sometimes you may not have access to the source code to do the proper convertion. In this case, there is some additional syntax overhead to be head, unless an implicit can be used. I'll give an example below, in which I use an "toOption
" function -- there is such a thing on Scala 2.8, of which I'll talk about below.
scala> def toOption[T](t: T): Option[T] = if (t == null) None else Some(t)
toOption: [T](t: T)Option[T]
scala> case class Address(city: String = null, street: String = null, number: Int = 0)
defined class Address
scala> case class Contact(phone: String = null, address: Address = null)
defined class Contact
scala> case class Person(name: String, contactDetails: Contact = null)
defined class Person
scala> case class Company(name: String, contactPerson: Person = null)
defined class Company
scala> val p1 = Company("ABC", Person("Dean", Contact(null, Address(city = "New England"))))
p1: Company = Company(ABC,Person(Dean,Contact(null,Address(New England,null,0))))
scala> val p2 = Company("Finnicky", Person("Gimli"))
p2: Company = Company(Finnicky,Person(Gimli,null))
scala> for(company <- List(p1, p2);
| contactPerson <- toOption(company.contactPerson);
| contactDetails <- toOption(contactPerson.contactDetails);
| address <- toOption(contactDetails.address);
| city <- toOption(address.city)) yield city
res30: List[String] = List(New England)
Remember that you can be quite creative in naming a function. So, instead of "toOption
", I might have named it "?
", in which case I'd write things like "?(address.city)
".
Thanks to nuttycom for reminding me, on Scala 2.8 there is an Option
factory on the object Option
, so I can just write Option(something)
. In effect, you can replace "toOption
" above with "Option
". And if you prefer using ?
, you can just use import
with rename.
Upvotes: 16
Reputation: 11606
How about this?
def ?[A](block: => A) =
try { block } catch {
case e: NullPointerException if e.getStackTrace()(2).getMethodName == "$qmark" => null
case e => throw e
}
Using this little snippet, you can dereference safely and the code itself is quite succinct:
val a = ?(b.c.d.e)
a == null if b or b.c or b.c.d or b.c.d.e is null, otherwise, a == b.c.d.e
I think the value of a safe-dereference operator is diminished when you are using a language like Scala which has facilities like call-by-name and implicits.
ps: I modify the code above a bit in light of one of the comments below to handle the case when NullPointerException is actually thrown inside the called function.
BTW, I think using the function below is a more idiomatic way of writing Scala:
def ??[A](block: => A): Option[A] = ?(block) match {
case a: A => Some(a)
case _ => None
}
like so:
??(a.b.c.d) match {
case Some(result) => // do more things with result
case None => // handle "null" case
}
Upvotes: 6
Reputation: 217
Monadic bind (flatMap/map) with the scala.Option type. Support is also provided by for-comprehensions. Scalaz provides an applicative functor style if you prefer.
This is not equivalent, but a far better solution than Groovy's operator for many reasons.
Upvotes: 6