2009-07-22 10 views
24

Me gustaría saber cuál es la mejor imitación de Scala de safe-dereference operator (?.) de Groovy, o al menos algunas alternativas cercanas?¿Mejor imitación de Scala del operador de desreferencia segura de Groovy (?.)?

he discussed it breiflyDaniel Spiewak en el blog 's, pero me gustaría abrirlo a StackOverflow ...

Por el bien de tiempo para todos, aquí está la respuesta de Daniel inicial, mi contador, y su segunda respuesta :

@Antony

Actually, I looked at doing that one first. Or rather, I was trying to replicate Ragenwald’s andand “operator” from Ruby land. The problem is, this is a bit difficult to do without proxies. Consider the following expression (using Ruby’s andand, but it’s the same with Groovy’s operator):

test.andand().doSomething()

I could create an implicit conversion from Any => some type implementing the andand() method, but that’s where the magic stops. Regardless of whether the value is null or not, the doSomething() method will still execute. Since it has to execute on some target in a type-safe manner, that would require the implementation of a bytecode proxy, which would be flaky and weird (problems with annotations, final methods, constructors, etc).

A better alternative is to go back to the source of inspiration for both andand as well as Groovy’s safe dereference operator: the monadic map operation. The following is some Scala syntax which uses Option to implement the pattern:

val something: Option[String] = … // presumably could be either Some(…) or None

val length = something.map(_.length)

After this, length either be Some(str.length) (where str is the String object contained within the Option), or None. This is exactly how the safe-dereferencing operator works, except it uses null rather than a type-safe monad.

As pointed out above, we could define an implicit conversion from some type T => Option[T] and then map in that fashion, but some types already have map defined, so it wouldn’t be very useful. Alternatively, I could implement something similar to map but with a separate name, but any way it is implemented, it will rely upon a higher-order function rather than a simple chained call. It seems to be just the nature of statically typed languages (if anyone has a way around this, feel free to correct me).

Daniel Spiewak Monday, July 7, 2008 at 1:42 pm

Mi segunda pregunta: ¿

Thanks for the response Daniel regarding ?. I think I missed it! I think I understand what you’re proposing, but what about something like this, assuming you don’t have control over the sources:

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

Say it’s a java bean and you can’t go in and change the return values to Something[T] - what can we do there?

Antony Stubbs Tuesday, July 21, 2009 at 8:07 pm oh gosh - ok on re-read that’s where you’re proposing the implicit conversion from T to Option[T] right? But would you still be able to chain it together like that? You’d still need the map right? hmm….

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

?

Antony Stubbs Tuesday, July 21, 2009 at 8:10 pm

Su segunda respuesta:

@Antony

We can’t really do much of anything in the case of company?.getContactPerson, etc… Even assuming this were valid Scala syntax, we would still need some way to prevent the later calls in the chain. This is not possible if we’re not using function values. Thus, something like map is really the only option.

An implicit conversion to Option wouldn’t be bad, but by making things implicit, we’re circumventing some of the protection of the type system. The best way to do this sort of thing is to use for-comprehensions in concert with Option. We can do map and flatMap, but it’s much nicer with magical syntax:

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

Daniel Spiewak Tuesday, July 21, 2009 at 9:28 pm

P. S. si Daniel publica sus respuestas originales en su blog como respuestas, editaré la pregunta para eliminarlas por el bien del Sistema.

+1

Ver también http://stackoverflow.com/questions/1364361/how-to-write-a-proper-null-safe-coalescing-operator-in-scala –

Respuesta

8

¿Qué tal esto?

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

El uso de este pequeño fragmento, se puede eliminar la referencia de seguridad y el código en sí es bastante sucinta:

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

a == null si B o C. o BCD o bcde es nula, por lo demás, a = = bcde

Creo que el valor de un operador de eliminación de referencias segura se ve disminuido cuando se utiliza un idioma como Scala que tiene funciones como llamadas por nombre e implícitos.

ps: Modifico un poco el código anterior a la luz de uno de los comentarios a continuación para manejar el caso cuando NullPointerException es realmente lanzado dentro de la función llamada.

Por cierto, creo que el uso de la función de abajo es una forma más idiomática de escribir Scala:

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

así:

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

¡Por supuesto! ¡Hermosa! Una vez más, ¡estoy realmente impresionado con Scala! ¡Esto es incluso mejor que el operador de Groovy! Me pregunto si puedes hacer lo mismo con los cierres en Groovy. Definitivamente va a agregar este a la caja de herramientas! Para aparecer pronto en las extensiones de Wicket-Scala ... ¡Gracias! –

+0

Una demostración: scala> def?[A] (bloque: => A) = | bloque try {} catch {Caso E: NullPointerException => null} $ Qmark: [A] (=> A) Cualquier Scala> caso clase dirección (ciudad: String) Scala> class Person caso (a: Dirección) Scala> clase caso de la empresa (p: Persona) Scala> var a = Company (Persona (Dirección ("c Aduanas"))) a: empresa = empresa (Persona (Dirección (st Aduanas))) Scala> var b = empresa (Persona (nulo)) b: empresa = empresa (Persona (nulo)) Scala> println (apacidad) st Aduanas Scala> println (b. p.a.city) java.lang.NullPointerException Scala> println (? (b.p.a.city)) nula –

+0

tal vez usted podría editar sus comentarios y añadir que en la demostración, pero con saltos de línea? –

6

Monadic bind (flatMap/map) con el tipo de scala.Option. El apoyo también es proporcionado por for-comprehensions. Scalaz proporciona un estilo de funcionador aplicativo si lo prefiere.

Esto no es equivalente, pero es una solución mucho mejor que el operador de Groovy por muchas razones.

+1

¿Puedes dar un ejemplo? –

+0

Un ejemplo: Algunos (5) .map (_ + 5) == Algunos (10) foo val: Opción [Int] = ninguno foo.map (_ + 5) == Ninguno –

+1

... y _how_ ¿es una solución mejor que un '? .'? –

12

Hay dos cosas que deben tenerse en cuenta aquí.

En primer lugar, está el problema del "nada". ¿Cómo encadenas cosas cuando una parte de la cadena puede no devolver nada? La respuesta es usando Option y for comprensiones. Por ejemplo:

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) 

Así se supone que debe escribir el código que puede devolver algo o no en Scala.

El segundo problema, por supuesto, es que a veces puede que no tenga acceso al código fuente para hacer la conversión correcta. En este caso, hay una sobrecarga de sintaxis adicional para ser cabeza, a menos que se pueda usar una implícita. Voy a dar un ejemplo a continuación, en el que utilizo una función "toOption" - existe tal cosa en Scala 2.8, de la que hablaré a continuación.

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) 

Recuerde que puede ser bastante creativo al nombrar una función. Entonces, en lugar de "toOption", podría haberlo llamado "?", en cuyo caso escribiría cosas como "?(address.city)".

Gracias a nuttycom por recordarme, en Scala 2.8 hay una fábrica de Option en el objeto Option, así que sólo pueden escribir Option(something). En efecto, puede reemplazar "toOption" arriba con "Option". Y si prefiere usar ?, puede simplemente usar import con cambiar el nombre.

+0

El método que está pensando en 2.8 es Option.apply(). Por lo tanto, al igual que tiene Some (a) y None, ahora tiene Option (a) que devuelve if (a == null) None else Some (a) –

+0

Dado 2.8, podría obtener su "?" simplemente haciendo scala de importación. {Opción =>?} –

2

Debido a que este se vería terrible como un comentario, he aquí una comentado versión del código de 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 
    } 

Y la especificación, que pasa:

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

No es mía, sino de un compañero

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

Para dar seguimiento a la respuesta de Daniel C. Sobral, la razón opción es la preferida porque es idiomática Scala no utiliza punteros nulos. Si puede, vuelva a escribir el código para devolver las Opciones en lugar de las referencias que aceptan nulos. Los mapas planos encadenados son más limpios que las comprensiones, ya que no necesita un nuevo nombre de variable para cada paso. Si todos los valores son opcionales (como en el ejemplo maravilloso), el enfoque Scala se vería así:

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

Si tiene que usar los valores con valores nulos para la interoperabilidad de Java, aquí es un enfoque que le da seguridad sin NPE-wrangling o demasiado desorden:

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

Esto debería ser fácil de ampliar a una mónada llena de estilo opción si se desea.

10

Crea esta conversión implícita.

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) 

El uso no es tan bonito como Groovy, pero no es horrible.

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 
0

me gusta el uso de Daniel C. Sobral por comprensiones --- se llega al punto más rápidamente que la cascada de match anidados es que había estado haciendo. Sin embargo, todavía no es muy conveniente porque todavía hay variables ficticias intermedias (y demasiada tipeo).

Queremos algo como a?.b?.c?.d, así que no tenemos que pensar en lo que viene en medio: simplemente trate de obtener algo y deme una Option en caso de que no pueda obtenerlo.

Para el contexto, supongamos que tengo

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

que quiero desempacar. La comprensión de irían como la siguiente

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

que se traduce en Some(123). El problema son demasiadas variables temporales (y el hecho de que está leyendo parcialmente hacia atrás).

Me resulta más fácil hacerlo con flatMap, como este

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

o

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

que también se traduce en Some(123).

Se podría reducir el nivel de detalle y el uso de la ? símbolo deseado, dando efectivamente el tipo Option un método ? que hace lo mismo que flatMap. Option está sellado de subclases, pero podemos simular el nuevo método con conversiones implícitas.

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 

Y luego

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

produce Some(123. Todavía no es perfecto porque hay corchetes anidados y guiones bajos que debes acertar, pero es mejor que cualquier otra alternativa que haya visto.

+0

Me acabo de dar cuenta de que D. Gates puede hacer esto sin los corchetes, pero mi consola Scala dice 'error: tipo de parámetro faltante para la función expandida ((x $ 1) => z.flatMap (x $ 1.y))' cuando intento 'x flatMap _.y'. A menudo encuentro estos errores y me veo obligado a agregar una sintaxis más decorativa. –

Cuestiones relacionadas