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

Mi piacerebbe sapere quale sia la migliore imitazione di Scala dell’operatore di Safe-dereference di Groovy (?.) , O almeno alcune alternative strette?

Ne ho parlato brevemente sul blog di Daniel Spiewak , 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 pensato di farlo prima. O meglio, stavo cercando di replicare l’andante e l’operatore di Ragenwald da Ruby land. Il problema è che questo è un po ‘difficile da fare senza i proxy. Prendi in considerazione la seguente espressione (usando l’android di Ruby, ma è lo stesso con l’operatore di Groovy):

test.andand (). doSomething ()

Potrei creare una conversione implicita da Any => qualche tipo che implementa il metodo andand (), ma è lì che si ferma la magia. Indipendentemente dal fatto che il valore sia nullo o no, il metodo doSomething () verrà comunque eseguito. Dal momento che deve eseguire su qualche target in modo sicuro dal tipo, ciò richiederebbe l’implementazione di un proxy bytecode, che sarebbe traballante e bizzarro (problemi con annotazioni, metodi finali, costruttori, ecc.).

Un’alternativa migliore è tornare alla fonte di ispirazione per entrambi e anche per l’operatore di dereferenziazione sicuro di Groovy: l’operazione della mappa monadica. Quanto segue è una syntax di Scala che usa Option per implementare il pattern:

val something: Option [String] = … // presumibilmente potrebbe essere Some (…) o None

val length = something.map (_. length)

Successivamente, la length può essere Some (str.length) (dove str è l’object String contenuto nell’opzione) o None. Questo è esattamente il modo in cui lavora l’operatore di “Safe-Dereferencing”, eccetto che usa una monade nulla al sicuro piuttosto che una cassaforte di tipo.

Come indicato sopra, potremmo definire una conversione implicita da qualche tipo T => Opzione [T] e quindi mappare in quel modo, ma alcuni tipi hanno già definito la mappa, quindi non sarebbe molto utile. In alternativa, potrei implementare qualcosa di simile alla mappa ma con un nome separato, ma in qualunque modo venga implementato, si baserà su una funzione di ordine superiore piuttosto che su una semplice chiamata concatenata. Sembra essere solo la natura delle lingue tipizzate staticamente (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! Penso di capire cosa stai proponendo, ma che dire di qualcosa del genere, assumendo che tu non abbia il controllo sulle fonti:

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

Diciamo che è un java bean e non puoi entrare e cambiare i valori di ritorno in Something [T] – cosa possiamo fare lì?

Antony Stubbs Martedì 21 luglio 2009 alle 20:07 oh gosh – ok per rileggere è dove stai proponendo la conversione implicita da T a Option [T] giusto? Ma saresti ancora in grado di metterlo insieme in quel modo? 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 fare molto di tutto nel caso della compagnia? .GetContactPerson, ecc … Anche supponendo che questa fosse una syntax di Scala valida, avremmo comunque bisogno di un modo per prevenire le chiamate successive nella catena. Questo non è ansible se non stiamo usando i valori delle funzioni. Quindi, qualcosa come la mappa è davvero l’unica opzione.

Una conversione implicita all’opzione non sarebbe male, ma rendendo le cose implicite, stiamo aggirando parte della protezione del sistema dei tipi. Il modo migliore per fare questo genere di cose è usare le incomprensioni di concerto con Option. Possiamo fare mappe e flatmap, ma è molto più bello con la syntax 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

Ps se Daniel posta le risposte originali sul suo blog come risposte, modificheremo la domanda per rimuoverle per il bene del sistema.

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 = ?(bcde) 

a == null se b or bc o bcd o bcde è nullo, altrimenti a == bcde

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 in cui NullPointerException viene effettivamente lanciato 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ì:

 ??(abcd) match { case Some(result) => // do more things with result case None => // handle "null" case } 

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 syntax, a meno che non si possa usare un implicito. Darò un esempio qui sotto, in cui uso una funzione " toOption " - 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 toOption " ? ", Nel qual caso scriverei cose come " ?(address.city) ".

Grazie a Nuttycom per avermelo ricordato, su Scala 2.8 c'è una Option factory sull'Opzione Option , quindi posso semplicemente scrivere Option(something) . In effetti, puoi sostituire " toOption " sopra con " Option ". E se preferisci usare ? , puoi semplicemente usare l' import con rinomina.

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 

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.

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

Per seguire la risposta di Daniel C. Sobral, la ragione per cui Opzione è preferita è perché la Scala idiomatica non usa puntatori nulli. Se è ansible, 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.

Perché questo sarebbe un commento terribile, ecco una versione commentata del codice di Walter:

 /** * Safe dereference operator. Eg ?(abcnull.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] } } 

Mi è piaciuto l’uso di Daniel C. Sobral per le comprensioni — arriva al punto più rapidamente della cascata di match annidati che 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: prova solo a ottenere qualcosa e dammi Option nel caso in cui tu non possa ottenerlo.

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 risulta anche in Some(123) .

Si potrebbe ridurre la verbosità e usare il desiderato ? simbolo dando effettivamente al tipo Option un metodo ? fa la stessa cosa di flatMap . Option è sigillata dalla sottoclass, 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}} 

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