Domanda Migliore imitazione di Scala dell'operatore Groovy per la sicurezza della dereferenza (?.)?


Mi piacerebbe sapere quale sia la migliore imitazione di Scala di Groovy operatore di sicurezza-dereferenziazione (?.)o almeno alcune alternative vicine sono?

Io ho discusso brevemente sopra Daniel Spiewakil blog, ma vorrei aprirlo a StackOverFlow ...

Per il tempo di tutti, ecco la risposta iniziale di Daniel, il mio contatore e la sua seconda risposta:

@Antony

In realtà, ho guardato a fare quello   primo. O meglio, stavo cercando di farlo   replicare l'andatura di Ragenwald   "Operatore" dalla terra di Ruby. Il problema   è, questo è un po 'difficile da fare   senza proxy. Considera il   seguente espressione (usando Ruby's   andand, ma è lo stesso con   Operatore di Groovy):

test.andand (). doSomething ()

Potrei creare una conversione implicita   da Qualsiasi => qualche tipo che implementa il   andand () metodo, ma questo è dove il   la magia si ferma Indipendentemente dal fatto che il   il valore è nullo o no, il   il metodo doSomething () funzionerà ancora   eseguire. Dal momento che deve essere eseguito   qualche obiettivo in un modo sicuro dal tipo,   ciò richiederebbe l'implementazione   di un proxy bytecode, quale sarebbe   traballante e strano (problemi con   annotazioni, metodi finali,   costruttori, ecc.).

Un'alternativa migliore è tornare a   la fonte di ispirazione per entrambi   e così come la cassaforte di Groovy   operatore di dereferenziazione: la mappa monadica   operazione. Quello che segue è un po 'di Scala   sintassi che utilizza l'opzione da implementare   Il modello:

val something: Option [String] = ... //   presumibilmente potrebbe essere Qualcuno (...) o   Nessuna

val length = something.map (_. length)

Dopodichè, length o essere   Alcuni (str.length) (dove str è il   Oggetto stringa contenuto all'interno di   Opzione) o Nessuno. Questo è esattamente come   l'operatore della sicurezza della dereferenzialità funziona,   tranne che usa null anziché a   monade sicura per i caratteri.

Come indicato sopra, potremmo definire   una conversione implicita da qualche tipo   T => Opzione [T] e quindi mappare in quello   moda, ma alcuni tipi hanno già   mappa definita, quindi non sarebbe molto   utile. In alternativa, potrei   implementare qualcosa di simile alla mappa ma   con un nome separato, ma in ogni caso   è implementato, si baserà su a   funzione di ordine superiore piuttosto che a   semplice chiamata concatenata. Sembra essere   solo la natura di tipizzazione statica   lingue (se qualcuno ha un modo per aggirare   questo, sentiti libero di correggermi).

Daniel Spiewak lunedì 7 luglio 2008 alle   13:42

La mia seconda domanda:

Grazie per la risposta Daniel   riguardo? Penso di averlo perso! io   penso di capire quello che sei   proponendo, ma per quanto riguarda qualcosa   in questo modo, supponendo che tu non abbia   controllo sulle fonti:

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

Dì che è un fagiolo java e non puoi andare   in e modificare i valori di ritorno in   Qualcosa [T] - cosa possiamo fare lì?

Antony Stubbs Martedì 21 luglio 2009   alle 8:07 pm oh gosh - ok per rileggere   è qui che stai proponendo il   conversione implicita da T a   Opzione [T] giusto? Ma lo faresti comunque   essere in grado di concatenarlo insieme   quella? Avresti ancora bisogno della mappa, giusto?   hmm ....

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

?

Antony Stubbs Martedì 21 luglio 2009   alle 20:10

La sua seconda risposta:

@Antony

Non possiamo davvero fare molto di niente in   il caso della compagnia? .getContactPerson,   ecc ... Anche supponendo che questo fosse valido   Sintassi di Scala, ne avremmo ancora bisogno   modo per prevenire le chiamate successive nel   catena. Questo non è possibile se lo siamo   non usando i valori delle funzioni. Così,   qualcosa come la mappa è davvero l'unica   opzione.

Una conversione implicita all'opzione   non sarebbe male, ma facendo le cose   implicito, stiamo aggirando alcuni di   la protezione del sistema di tipi. Il   il modo migliore per fare questo genere di cose è   usare per-comprensione in concerto   con Opzione. Possiamo fare la mappa e   flatMap, ma è molto più bello con   sintassi magica:

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

Daniel Spiewak martedì 21 luglio 2009 alle 21:28

Post scriptum se Daniel pubblica le sue risposte originali sul suo blog come risposte, modificheremo la domanda per rimuoverle per il bene del sistema.


24
2017-07-22 06:31


origine


risposte:


Cosa ne pensi di questo?

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

Usando questo piccolo frammento, puoi dereferenziare in sicurezza e il codice stesso è abbastanza succinto:

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

a == null se b o b.c o b.c.d o b.c.d.e è nullo, altrimenti a == b.c.d.e

Penso che il valore di un operatore di sicurezza-dereferenza sia diminuito quando si utilizza un linguaggio come Scala che ha funzionalità come call-by-name e impliciti.

ps: modifico il codice sopra un bit alla luce di uno dei commenti seguenti per gestire il caso quando NullPointerException è effettivamente gettato all'interno della funzione chiamata.

A proposito, penso che usare la funzione qui sotto sia un modo più idiomatico di scrivere Scala:

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

così:

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

8
2017-07-24 16:57



Ci sono due cose che devono essere considerate qui.

Innanzitutto, c'è il problema del "niente". Come si concatenano le cose quando una parte della catena non può restituire nulla? La risposta sta usando Option e for comprensioni. Per esempio:

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)

Ecco come si suppone di scrivere codice che possa restituire qualcosa o meno in Scala.

Il secondo problema, naturalmente, è che a volte potresti non avere accesso al codice sorgente per fare la conversione corretta. In questo caso, c'è un po 'di overhead aggiuntivo per la sintassi, a meno che non si possa usare un implicito. Darò un esempio qui sotto, in cui uso un "toOption"function - c'è una cosa del genere su Scala 2.8, di cui parlerò di seguito.

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)

Ricorda che puoi essere abbastanza creativo nel nominare una funzione. Quindi, invece di "toOption", Avrei potuto chiamarlo"?", nel qual caso scriverei cose come"?(address.city)".

Grazie a nuttycom per avermelo ricordato, su Scala 2.8 c'è un Option fabbrica sull'oggetto Option, quindi posso solo scrivere Option(something). In effetti, puoi sostituire "toOption"sopra con"Option"E se preferisci usare ?, puoi semplicemente usare import con rinomina.


13
2017-07-22 14:43



Crea questa conversione implicita.

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)

L'uso non è bello come Groovy, ma non è terribile.

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

10
2018-04-06 16:37



Bind monadico (flatMap / map) con il tipo scala.Option. Il supporto è fornito anche da incomprensioni. Scalaz fornisce uno stile di funtore applicativo se preferisci.

Questo non è equivalente, ma una soluzione molto migliore rispetto all'operatore di Groovy per molte ragioni.


6
2017-07-22 11:21



Non mio ma di un collega

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

4
2017-11-13 05:49



Per seguire la risposta di Daniel C. Sobral, la ragione per cui Opzione è preferita è perché la Scala idiomatica non usa puntatori nulli. Se è possibile, riscrivi il codice per restituire le opzioni anziché i riferimenti nullable. Le flatmap concatenate sono più pulite delle incomprensioni, poiché non è necessario un nuovo nome di variabile per ogni passaggio. Se tutti i valori sono opzionali (come nell'esempio di Groovy), l'approccio di Scala sarà simile a questo:

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

Se è necessario utilizzare valori nullable per l'interoperabilità Java, ecco un approccio che offre sicurezza senza conflitti con NPE o troppa confusione:

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

Questo dovrebbe essere facile da estendere a una monade completa in stile Option, se lo si desidera.


4
2018-01-29 16:06



Perché questo sarebbe un commento terribile, ecco una versione commentata del codice di 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
  }

E la specifica, che passa:

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

2
2017-07-30 03:17



Mi è piaciuto l'uso di Daniel C. Sobral per le comprensioni --- arriva al punto più rapidamente della cascata di nidificati matchche stavo facendo Tuttavia, non è ancora molto comodo perché ci sono ancora variabili dummy intermedie (e troppa digitazione).

Vogliamo qualcosa come a?.b?.c?.d quindi non dobbiamo pensare a ciò che viene in mezzo: cerca solo di ottenere qualcosa e dammelo Option nel caso in cui non riesci a prenderlo.

Per contesto, supponiamo di averlo

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

che voglio disfare i bagagli. La comprensione per andare sarebbe come la seguente

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

quale risulta in Some(123). Il problema sono troppe variabili temporanee (e il fatto che sia parzialmente leggibile all'indietro).

Trovo più facile farlo con flatMap, come questo

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

o

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

che si traduce anche in Some(123).

Si potrebbe ridurre la verbosità e usare il desiderato ? simbolo dando effettivamente il Option digitare un metodo ? fa la stessa cosa di flatMap. Option è sigillato dalla sottoclasse, ma possiamo simulare il nuovo metodo con conversioni implicite.

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

E poi

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

i rendimenti Some(123. Non è ancora perfetto perché ci sono parentesi e underscore nidificati che devi correggere, ma è meglio di qualsiasi altra alternativa che ho visto.


0
2017-07-01 17:33