Лучшая имитация Scala оператора безопасной разыменования в Groove (?.)? - PullRequest
25 голосов
/ 22 июля 2009

Я хотел бы знать, какова лучшая имитация Scala оператора безопасного разыменования Groovy (?.) , или хотя бы несколько близких альтернатив?

Я кратко обсудил это в блоге Daniel Spiewak , но хотел бы открыть его для StackOverFlow ...

Ради времени каждого, вот первоначальный ответ Даниэля, мой счетчик и его второй ответ:

@ Antony

На самом деле, я смотрел на это первый. Вернее, я пытался повторить Ragenwald's andand «Оператор» из рубиновой земли. Эта проблема это немного сложно сделать без прокси. Рассмотрим следующее выражение (используя Ruby's и, но то же самое с Оператор Groovy):

test.andand (). ЙоЗотеЬЫпд ()

Я мог бы создать неявное преобразование от Any => некоторый тип, реализующий andand (), но именно там магия останавливается. Независимо от того, значение равно нулю или нет, Метод doSomething () будет по-прежнему выполнить. Поскольку он должен выполняться на некоторая цель безопасным для типа образом, что потребует реализации байт-кода прокси, который будет странный и странный (проблемы с аннотации, финальные методы, конструкторы и т. д.).

Лучшая альтернатива - вернуться к источник вдохновения для обоих и а также сейф Groovy оператор разыменования: монадическая карта операция. Ниже приведено несколько Scala синтаксис, который использует Option для реализации шаблон:

val что-то: Option [String] =… // предположительно, может быть либо Some (…), либо Ни один

val длина = что-то. Карта (_. Длина)

После этого length либо будет Некоторые (str.length) (где str - это Строковый объект, содержащийся в Опция) или Нет. Это именно так работает оператор безопасного разыменования, за исключением того, что использует нуль, а не типобезопасная монада.

Как указывалось выше, мы могли бы определить неявное преобразование из некоторого типа T => Option [T], а затем отобразить в этом мода, но некоторые типы уже есть карта определена, поэтому она не будет полезно. В качестве альтернативы я мог бы реализовать что-то похожее на карту, но с отдельным именем, но в любом случае это реализован, он будет опираться на функция высшего порядка, а не простой цепной вызов. Это кажется просто характер статически типизирован языки (если у кого есть способ обойти не стесняйтесь поправлять меня).

Даниэль Спивак Понедельник, 7 июля 2008 г. в 13:42

Мой второй вопрос:

Спасибо за ответ, Даниэль относительно? Я думаю, что я пропустил это! я кажется, я понимаю, что ты предлагая, но как насчет чего-то как это, если у вас нет контроль над источниками:

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

Скажите, что это боб Java, и вы не можете идти и измените возвращаемые значения на Что-то [T] - что мы можем сделать там?

Энтони Стаббс Вторник, 21 июля 2009 г. в 20:07 о боже - хорошо, перечитайте вот где вы предлагаете неявное преобразование из T в Вариант [T] верно? Но вы все равно быть в состоянии связать это вместе, как тот? Вам все еще нужна карта, верно? хмм ....

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

Энтони Стаббс Вторник, 21 июля 2009 г. в 8:10 вечера

Его второй ответ:

@ Antony

Мы ничего не можем сделать в дело компании? .getContactPerson, и т.д.… Даже если предположить, что это действительно Синтаксис Scala, нам все еще нужно немного способ предотвратить последующие звонки в цепь. Это невозможно, если мы не используя значения функций. Таким образом, что-то вроде карты действительно единственный опция.

Неявное преобразование в Option не было бы плохо, но делая вещи неявно, мы обходим некоторые из защита системы типов. Лучшийспособ сделать такую ​​вещь использовать для понимания в концерте с опцией. Мы можем сделать карту и flatMap, но гораздо приятнее с магический синтаксис:

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

Даниэль Спивак Вторник, 21 июля 2009 г., 21:28

P.s. если Дэниел разместит свои оригинальные ответы в своем блоге в качестве ответов, я отредактирую вопрос, чтобы удалить их ради Системы.

Ответы [ 8 ]

15 голосов
/ 22 июля 2009

Здесь необходимо учитывать две вещи.

Во-первых, существует проблема «ничего». Как вы цепляете вещи, когда часть цепочки может ничего не возвращать? Ответ использует понимание 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 с переименованием.

10 голосов
/ 06 апреля 2011

Создать это неявное преобразование.

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
8 голосов
/ 24 июля 2009

Как насчет этого?

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 или b.c или b.c.d или b.c.d.e равны нулю, в противном случае a == b.c.d.e

Я думаю, что значение оператора безопасной разыменения уменьшается, когда вы используете такой язык, как Scala, который имеет такие функции, как call-by-name и implicits.

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
  }
6 голосов
/ 22 июля 2009

Монадное связывание (flatMap / map) с типом scala.Option. Поддержка также предоставляется для понимания. Scalaz предлагает аппликативный стиль функтора, если вы предпочитаете.

Это не эквивалентно, но по многим причинам гораздо лучше, чем оператор Groovy.

4 голосов
/ 29 января 2011

Чтобы ответить на вопрос Дэниела С. Собрала, предпочтительным вариантом является то, что идиоматический 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.

4 голосов
/ 13 ноября 2009

Не мой, а сотрудник

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")
2 голосов
/ 30 июля 2009

Поскольку это будет выглядеть ужасно как комментарий, вот прокомментированная версия кода Уолтера:

/**
 * 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]
  }   
}
0 голосов
/ 01 июля 2015

Мне понравилось использование Даниэля С. Собрала для понимания - оно достигает сути быстрее, чем каскад вложенных match es, который я делал. Однако, это все еще не очень удобно, потому что все еще есть промежуточные фиктивные переменные (и слишком много печатания).

Мы хотим что-то вроде 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. Это все еще не идеально, потому что есть вложенные скобки и подчеркивания, которые вы должны сделать правильно, но это лучше, чем любые альтернативы, которые я видел.

...