我想知道最好的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在他的博客上发布他的原始答案作为答案,我会编辑问题以便为系统删除它们。
答案 0 :(得分:15)
这里有两件事需要考虑。
首先,存在“无”的问题。当链条的一部分可能不返回任何东西时,你如何链接东西?答案是使用Option
和for
理解。例如:
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
。它仍然不完美,因为有嵌套的括号和下划线,你必须正确,但它比我见过的任何替代品都好。