Лучшая имитация Scala оператора безопасной разыменования (?.)
Я хотел бы знать, какова лучшая имитация Scala оператора безопасной разыменования Groovy (?.) Или хотя бы какие-нибудь близкие альтернативы?
Я кратко обсудил это в блоге Daniel Spiewak, но хотел бы открыть его для StackOverFlow...
Ради времени всех, вот первоначальный ответ Дэниела, мой счетчик и его второй ответ:
@Antony
На самом деле, я сначала посмотрел на это. Вернее, я пытался скопировать Рагенвальда и "оператора" из земли Руби. Проблема в том, что это немного сложно сделать без прокси. Рассмотрим следующее выражение (с использованием Ruby's andand, но то же самое с оператором Groovy):
test.andand(). DoSomething()
Я мог бы создать неявное преобразование из Any => некоторого типа, реализующего метод andand(), но на этом магия останавливается. Независимо от того, является ли значение нулевым или нет, метод doSomething() все равно будет выполняться. Поскольку он должен выполняться для некоторой цели типобезопасным способом, для этого потребуется реализация прокси-сервера с байт-кодом, который будет нестабильным и странным (проблемы с аннотациями, конечными методами, конструкторами и т. Д.).
Лучшая альтернатива - вернуться к источнику вдохновения для обоих, а также к безопасному оператору разыменования Groovy: операции монадической карты. Ниже приведен некоторый синтаксис Scala, который использует Option для реализации шаблона:
val что-то: Option[String] = … // предположительно может быть либо Some(…), либо None
val длина = что-то. карта (_. длина)
После этого,
length
либо может быть Some(str.length) (где str - объект String, содержащийся в Option), либо None. Именно так работает оператор безопасной разыменования, за исключением того, что он использует нуль, а не типобезопасную монаду.Как указывалось выше, мы можем определить неявное преобразование из некоторого типа T => Option[T], а затем отобразить таким образом, но для некоторых типов уже определена карта, поэтому это будет не очень полезно. В качестве альтернативы, я мог бы реализовать что-то похожее на map, но с отдельным именем, но в любом случае это будет зависеть от функции более высокого порядка, а не от простого цепного вызова. Кажется, это просто природа статически типизированных языков (если у кого-то есть способ обойти это, не стесняйтесь меня поправлять).
Даниэль Спивак Понедельник, 7 июля 2008 года в 13:42
Мой второй вопрос:
Спасибо за ответ Дэниел относительно? Я думаю, что я пропустил это! Я думаю, что понимаю, что вы предлагаете, но что-то вроде этого, при условии, что у вас нет контроля над источниками:
company?.getContactPerson?.getContactDetails?.getAddress?.getCity
Скажем, это Java-бин, и вы не можете войти и изменить возвращаемые значения на Something[T] - что мы можем там сделать?
Энтони Стаббс Вторник, 21 июля 2009 года, в 20:07, о, черт возьми - хорошо, перечитайте, вот где вы предлагаете неявное преобразование из T в Option [T], верно? Но вы все еще сможете связать это вместе? Вам все еще нужна карта, верно? хмм....
var city = company.map(_.getContactPerson.map(_.getContactDetails.map(_.getAddress.map(_.getCity))))
?
Энтони Стаббс Вторник, 21 июля 2009 года, в 20:10
Его второй ответ:
@Antony
Мы ничего не можем сделать в случае компании?.GetContactPerson и т. Д. Даже если предположить, что это правильный синтаксис Scala, нам все равно потребуется какой-то способ предотвратить последующие вызовы в цепочке. Это невозможно, если мы не используем функциональные значения. Таким образом, что-то вроде карты действительно единственный вариант.
Неявное преобразование в Option не будет плохим, но, делая вещи неявными, мы обойдем часть защиты системы типов. Лучший способ сделать это - использовать для понимания в сочетании с Option. Мы можем сделать map и flatMap, но это намного лучше с магическим синтаксисом:
for {
c < - company
person <- c.getContactPerson
details <- person.getContactDetails
address <- details.getAddress
} yield address.getCity
Даниэль Спивак Вторник, 21 июля 2009 года в 21:28
PS Если Даниэль разместит свои оригинальные ответы в своем блоге в качестве ответов, я отредактирую вопрос, чтобы удалить их ради Системы.
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 == ноль, если b или bc или bcd или bcde равны нулю, в противном случае a == bcde
Я думаю, что ценность оператора безопасного разыменения уменьшается, когда вы используете такой язык, как 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
}
Есть две вещи, которые необходимо учитывать здесь.
Во-первых, существует проблема "ничего". Как вы цепляете вещи, когда часть цепочки может ничего не возвращать? Ответ использует 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
с переименованием.
Создайте это неявное преобразование.
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
Монадное связывание (flatMap/map) с типом scala.Option. Поддержка также предоставляется для понимания. Scalaz предоставляет аппликативный стиль функтора, если вы предпочитаете.
Это не эквивалентно, но гораздо лучшее решение, чем оператор Groovy по многим причинам.
Не мой, а сотрудник
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")
Чтобы продолжить ответ Даниэля С. Собрала, предпочтительным вариантом является то, что идиоматическая Scala не использует нулевые указатели. Если вы можете, переписать код, чтобы вернуть параметры вместо пустых ссылок. Цепные flatMaps более чисты, чем для понимания, так как вам не нужно новое имя переменной для каждого шага. Если все значения являются необязательными (как в примере Groovy), подход Scala будет выглядеть следующим образом:
(company flatMap _.getContactPerson
flatMap _.getContactDetails
flatMap _.getAddress
flatMap _.getCity) match {
case Some(city) => ...
case None => ...
}
Если вы должны использовать значения Nullable для взаимодействия 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.
Поскольку это будет выглядеть ужасно как комментарий, вот прокомментированная версия кода Уолтера:
/**
* 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]
}
}
Мне понравилось использование Даниэля С. Собрала для понимания - оно доходит до сути быстрее, чем каскад вложенных match
Я делал это. Однако, это все еще не очень удобно, потому что есть все еще промежуточные фиктивные переменные (и слишком много печатания).
Мы хотим что-то вроде a?.b?.c?.d
поэтому нам не нужно думать о том, что происходит между ними: просто попытайтесь что-то получить и дать мне Option
в случае, если вы не можете получить это.
Для контекста, предположим, у меня есть
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
, Это все еще не идеально, потому что есть вложенные скобки и подчеркивания, которые вы должны сделать правильно, но это лучше, чем любые альтернативы, которые я видел.