Best Scala imitation of Groovy's safe-dereference operator (?.)?

前端 未结 8 2022
心在旅途
心在旅途 2020-12-02 12:43

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 o

相关标签:
8条回答
  • 2020-12-02 13:03

    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.

    0 讨论(0)
  • 2020-12-02 13:04

    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.

    0 讨论(0)
  • 2020-12-02 13:08

    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
    
    0 讨论(0)
  • 2020-12-02 13:10

    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]
      }   
    }
    
    0 讨论(0)
  • 2020-12-02 13:15

    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
      }
    
    0 讨论(0)
  • 2020-12-02 13:18

    I liked Daniel C. Sobral's use of for comprehensions--- it gets to the point more quickly than the cascade of nested matches 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.

    0 讨论(0)
提交回复
热议问题