最好的Scala模仿Groovy的安全解除引用运算符(?。)?

时间:2009-07-22 06:31:52

标签: scala groovy

我想知道最好的Scala模仿Groovy的safe-dereference operator (?.),或者至少是一些接近的替代品?

我在discussed it breifly的博客上Daniel Spiewak,但想将其打开到StackOverFlow ......

为了每个人的时间,这是丹尼尔最初的回应,我的反击,以及他的第二个回应:

  

@Antony

     

实际上,我看着那样做   第一。或者说,我试图   复制Ragenwald的andand   来自Ruby的“运营商”。问题   是的,这有点难   没有代理人。考虑一下   下面的表达式(使用Ruby的   和,但是它是一样的   Groovy的运营商):

     

test.andand()。doSomething的()

     

我可以创建一个隐式转换   来自Any =>某种类型的实施   andand()方法,但那就是   魔术停止。无论是否   值为null或不为   doSomething()方法仍然会   执行。因为它必须执行   一些类型安全的目标,   这将需要实施   一个字节码代理,它将是   片状和怪异(问题与   注释,最终方法,   施工人员等。)

     

更好的选择是回归   两者的灵感来源   而且还有Groovy的安全   解引用算子:monadic地图   操作。以下是一些Scala   使用Option实现的语法   模式:

     

val something:Option [String] = ... //   大概可能是某些(...)或   无

     

val length = something.map(_。length)

     

在此之后,length也是   一些(str.length)(其中str是   包含在的字符串对象   选项),或无。这正是如何   安全解除引用操作员工作,   除了它使用null而不是a   类型安全的monad。

     

如上所述,我们可以定义   某种类型的隐式转换   T =>选项[T]然后映射到那里   时尚,但有些类型已经有   地图定义,所以它不会很   有用。或者,我可以   实现类似于map的东西但是   有一个单独的名称,但无论如何   实施后,它将依赖于   高阶函数而不是a   简单的链式电话。好像是   只是静态类型的本质   语言(如果有人有办法解决的话)   这个,请随意纠正我。)

     

Daniel Spiewak,2008年7月7日星期一   下午1:42

我的第二个问题:

  

感谢Daniel的回应   关于?我想我错过了!一世   我想我明白你的意思   提议,但是什么呢   像这样,假设你没有   控制来源:

company?.getContactPerson?.getContactDetails?.getAddress?.getCity
  

说它是一个java bean,你不能去   in并将返回值更改为   东西[T] - 我们能做什么?

     

Antony Stubbs,2009年7月21日,星期二   在晚上8:07哦天哪 - 好重读   这就是你提出的建议   隐式转换从T到   选项[T]对吗?但你还会吗?   能够把它连在一起像   那?你还需要地图吗?   嗯...。

var city = company.map(_.getContactPerson.map(_.getContactDetails.map(_.getAddress.map(_.getCity))))
  

     

Antony Stubbs,2009年7月21日,星期二   晚上8:10

他的第二个回应:

  

@Antony

     

我们无法做任何事情   公司的情况?.getContactPerson,   等等......即使假设这是有效的   Scala语法,我们仍然需要一些   防止以后调用的方法   链。如果我们这样做,这是不可能的   不使用函数值。从而,   像地图这样的东西真的是唯一的   选项。

     

隐式转换为Option   不会坏,但通过制造东西   隐含的,我们正在规避一些   保护类型系统。该   做这种事情的最好方法是   在演唱会中使用for-understanding   与选项。我们可以做地图和   flatMap,但它更好   神奇的语法:

 for {
   c < - company
   person <- c.getContactPerson   
   details <- person.getContactDetails
   address <- details.getAddress 
  } yield address.getCity
  

Daniel Spiewak,2009年7月21日星期二晚上9:28

P.S。如果Daniel在他的博客上发布他的原始答案作为答案,我会编辑问题以便为系统删除它们。

8 个答案:

答案 0 :(得分:15)

这里有两件事需要考虑。

首先,存在“无”的问题。当链条的一部分可能不返回任何东西时,你如何链接东西?答案是使用Optionfor理解。例如:

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)

这就是你应该如何编写可能在Scala中返回某些内容的代码。

第二个问题当然是有时你可能无法访问源代码来进行正确的转换。在这种情况下,除非可以使用隐式,否则还有一些额外的语法开销。我将在下面给出一个示例,其中我使用“toOption”函数 - 在Scala 2.8上有这样的东西,我将在下面讨论它。

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)

请记住,在命名函数时,您可以非常有创意。因此,我可能将其命名为“toOption”而不是“?”,在这种情况下,我会写“?(address.city)”之类的内容。

感谢nuttycom提醒我,在Scala 2.8上,对象Option上有一个Option工厂,所以我可以写Option(something)。实际上,您可以将上面的“toOption”替换为“Option”。如果您更喜欢使用?,则只需使用import重命名。

答案 1 :(得分:10)

创建此隐式转换。

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)

用法并不像Groovy那么漂亮,但它并不可怕。

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

答案 2 :(得分:8)

这个怎么样?

def ?[A](block: => A) =
  try { block } catch {
    case e: NullPointerException if e.getStackTrace()(2).getMethodName == "$qmark" => null
    case e => throw e
  }

使用这个小片段,您可以安全地取消引用,代码本身非常简洁:

val a = ?(b.c.d.e)

a == null如果b或b.c或b.c.d或b.c.d.e为null,否则,a == b.c.d.e

我认为当您使用Scala之类的语言时,安全解除引用运算符的值会减少,而Scala具有逐个调用和隐含的功能。

ps:根据以下评论之一修改上面的代码以处理NullPointerException时的情况 实际上抛出了被调用的函数。

顺便说一下,我认为使用下面的函数是一种更为惯用的Scala编写方式:

def ??[A](block: => A): Option[A] = ?(block) match {
    case a: A => Some(a)
    case _ => None
  }
像这样:

??(a.b.c.d) match {
    case Some(result) => // do more things with result
    case None => // handle "null" case
  }

答案 3 :(得分:6)

使用scala.Option类型的monadic绑定(flatMap / map)。 for-comprehensions也提供支持。如果您愿意,Scalaz提供了一种应用程序仿函数。

这不是等价的,但是出于很多原因,这是一个比Groovy运算符好得多的解决方案。

答案 4 :(得分:4)

不是我的,而是同事的

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")

答案 5 :(得分:4)

为了跟进Daniel C. Sobral的回答,选择首选的原因是因为惯用的Scala不使用空指针。如果可以,重写代码以返回选项而不是可空引用。链式flatMaps比理解更清晰,因为每个步骤都不需要新的变量名。如果所有值都是可选的(如Groovy示例中所示),则Scala方法如下所示:

(company flatMap _.getContactPerson
         flatMap _.getContactDetails
         flatMap _.getAddress
         flatMap _.getCity) match {
  case Some(city) => ...
  case None       => ...
}

如果必须使用可空值来实现Java互操作性,这里的方法可以让您在没有NPE争吵或太杂乱的情况下获得安全性:

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 _         => ...
}

如果需要,这应该很容易扩展为完整的Option-style monad。

答案 6 :(得分:2)

因为评论看起来很糟糕,这里是Walter代码的评论版本:

/**
 * 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
  }

规范,通过:

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]
  }   
}

答案 7 :(得分:0)

我喜欢Daniel C. Sobral对于理解的使用 - 它比我曾经做过的嵌套match级联更快地达到了这一点。但是,它仍然不是很方便,因为仍然存在中间虚拟变量(并且输入太多)。

我们需要像a?.b?.c?.d这样的内容,所以我们不必考虑介于两者之间的内容:只是尝试获取内容并给我一个Option以防万一你无法得到它。 / p>

对于上下文,假设我有

case class Inner(z: Option[Int])
case class Outer(y: Option[Inner])
val x = Some(Outer(Some(Inner(Some(123)))))

我要打开包装。理解将如下所示

for (tmp1 <- x; tmp2 <- tmp1.y; tmp3 <- tmp2.z) yield tmp3

会产生Some(123)。问题是有太多临时变量(以及它部分向后读取的事实)。

我发现使用flatMap更容易,就像这样

x.flatMap(_.y.flatMap(_.z))

x flatMap {_.y flatMap {_.z}}

也会产生Some(123)

可以通过有效地为?类型提供与Option完全相同的方法?来减少详细程度并使用所需的flatMap符号。 Option与子类化是密封的,但我们可以使用隐式转换来模拟新方法。

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

然后

x ? {_.y ? {_.z}}

收益Some(123。它仍然不完美,因为有嵌套的括号和下划线,你必须正确,但它比我见过的任何替代品都好。