Подтвердить что ты не робот

Лучшая Scala имитация оператора Groovy безопасного разыменования (?.)?

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

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

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

@Antony

Собственно, я смотрел на это первый. Вернее, я пытался реплицировать Ragenwalds andand "оператор" с земли Руби. Проблема это, это немного сложно сделать без прокси. Рассмотрим следующее выражение (используя Rubys и, и то же самое с Groovy s):

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

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

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

val something: Option [String] =...// предположительно, может быть либо Некоторое (...), либо Отсутствует

val length = something.map(_. length)

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

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

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

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

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

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

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

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

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

?

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

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

@Antony

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

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

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

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

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

4b9b3361

Ответ 1

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

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, который имеет такие функции, как 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
  }

Ответ 2

Здесь нужно рассмотреть две вещи.

Во-первых, есть проблема "ничего". Как вы связываете вещи, когда часть цепочки ничего не может вернуть? Ответ заключается в использовании понятий 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 factory на объекте Option, поэтому я могу просто написать Option(something). Фактически вы можете заменить "toOption" выше на "Option". И если вы предпочитаете использовать ?, вы можете просто использовать import с переименованием.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

Не мой, а коллега

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

Ответ 6

Чтобы ответить на вопрос Даниэля С. Собраля, причина Опция предпочтительна, потому что идиоматический Scala не использует нулевые указатели. Если вы можете, перепишите код, чтобы возвращать опции вместо нулевых ссылок. Связанные плоские карты чище, чем для понятий, поскольку для каждого шага вам не требуется новое имя переменной. Если все значения являются необязательными (как в примере Groovy), подход Scala будет выглядеть следующим образом:

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

Если вы должны использовать значения NULL для взаимодействия с 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, если это необходимо.

Ответ 7

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

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

Ответ 8

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