No puedo entender el sentido de la Option[T]
clase en Scala. Quiero decir, no puedo ver ninguna ventaja de None
más null
.
Por ejemplo, considere el código:
object Main{
class Person(name: String, var age: int){
def display = println(name+" "+age)
}
def getPerson1: Person = {
// returns a Person instance or null
}
def getPerson2: Option[Person] = {
// returns either Some[Person] or None
}
def main(argv: Array[String]): Unit = {
val p = getPerson1
if (p!=null) p.display
getPerson2 match{
case Some(person) => person.display
case None => /* Do nothing */
}
}
}
Ahora suponga que el método getPerson1
regresa null
, luego la llamada realizada a display
en la primera línea de main
es inevitable que falle NPE
. De manera similar, si getPerson2
regresa None
, la display
llamada fallará nuevamente con algún error similar.
Si es así, ¿por qué Scala complica las cosas al introducir un nuevo contenedor de valor ( Option[T]
) en lugar de seguir un enfoque simple utilizado en Java?
ACTUALIZAR:
He editado mi código según la sugerencia de @Mitch . Todavía no puedo ver ninguna ventaja en particular Option[T]
. Tengo que probar lo excepcional null
o None
en ambos casos. :(
Si he entendido correctamente la respuesta de @ Michael , ¿la única ventaja Option[T]
es que le dice explícitamente al programador que este método podría devolver None ? ¿Es esta la única razón detrás de esta elección de diseño?
get
y lo obtendrá . :-)Respuestas:
Obtendrá el punto de
Option
mejor si se obliga a nunca, nunca, usarget
. Eso es porqueget
es el equivalente de "ok, envíame de vuelta a tierra nula".Entonces, tome ese ejemplo suyo. ¿Cómo llamarías
display
sin usarget
? Aquí hay algunas alternativas:getPerson2 foreach (_.display) for (person <- getPerson2) person.display getPerson2 match { case Some(person) => person.display case _ => } getPerson2.getOrElse(Person("Unknown", 0)).display
Ninguna de estas alternativas le permitirá recurrir
display
a algo que no existe.En cuanto a por qué
get
existe, Scala no le dice cómo debe escribirse su código. Puede que te empuje suavemente, pero si no quieres recurrir a ninguna red de seguridad, es tu elección.Lo clavaste aquí:
Excepto por el "único". Pero permítanme repetirlo de otra manera: la principal ventaja de
Option[T]
overT
es la seguridad de tipos. Garantiza que no enviará unT
método a un objeto que puede no existir, ya que el compilador no se lo permitirá.Dijiste que tienes que probar la nulabilidad en ambos casos, pero si olvidas, o no sabes, tienes que verificar si hay nulos, ¿te lo dirá el compilador? ¿O sus usuarios?
Por supuesto, debido a su interoperabilidad con Java, Scala permite nulos al igual que lo hace Java. Por lo tanto, si usa bibliotecas de Java, si usa bibliotecas Scala mal escritas o si usa bibliotecas Scala personales mal escritas , aún tendrá que lidiar con punteros nulos.
Otras dos ventajas importantes de las
Option
que puedo pensar son:Documentación: una firma de tipo de método le dirá si un objeto siempre se devuelve o no.
Composibilidad monádica.
El último tarda mucho más en apreciarse por completo y no se adapta bien a ejemplos simples, ya que solo muestra su fuerza en código complejo. Entonces, daré un ejemplo a continuación, pero soy muy consciente de que difícilmente significará nada excepto para las personas que ya lo entienden.
for { person <- getUsers email <- person.getEmail // Assuming getEmail returns Option[String] } yield (person, email)
fuente
get
" -> Entonces, en otras palabras: "¡No lo hacesget
!" :)Comparar:
val p = getPerson1 // a potentially null Person val favouriteColour = if (p == null) p.favouriteColour else null
con:
val p = getPerson2 // an Option[Person] val favouriteColour = p.map(_.favouriteColour)
La propiedad monádica bind , que aparece en Scala como la función de mapa , nos permite encadenar operaciones sobre objetos sin preocuparnos de si son 'nulos' o no.
Lleve este simple ejemplo un poco más lejos. Digamos que queríamos encontrar todos los colores favoritos de una lista de personas.
// list of (potentially null) Persons for (person <- listOfPeople) yield if (person == null) null else person.favouriteColour // list of Options[Person] listOfPeople.map(_.map(_.favouriteColour)) listOfPeople.flatMap(_.map(_.favouriteColour)) // discards all None's
O tal vez nos gustaría encontrar el nombre de la hermana de la madre del padre de una persona:
// with potential nulls val father = if (person == null) null else person.father val mother = if (father == null) null else father.mother val sister = if (mother == null) null else mother.sister // with options val fathersMothersSister = getPerson2.flatMap(_.father).flatMap(_.mother).flatMap(_.sister)
Espero que esto arroje algo de luz sobre cómo las opciones pueden hacer la vida un poco más fácil.
fuente
map
volveráNone
y la llamada fallará con algún error. ¿Cómo es mejor que elnull
enfoque?La diferencia es sutil. Tenga en cuenta que para ser realmente una función, debe devolver un valor; nulo no se considera realmente un "valor de retorno normal" en ese sentido, más bien un tipo inferior / nada.
Pero, en un sentido práctico, cuando llamas a una función que opcionalmente devuelve algo, harías:
getPerson2 match { case Some(person) => //handle a person case None => //handle nothing }
Por supuesto, puede hacer algo similar con null, pero esto hace que la semántica de la llamada sea
getPerson2
obvia en virtud del hecho de que devuelveOption[Person]
(una buena cosa práctica, aparte de confiar en que alguien lea el documento y obtenga un NPE porque no lee el Doc).Intentaré encontrar un programador funcional que pueda dar una respuesta más estricta que yo.
fuente
Para mí, las opciones son realmente interesantes cuando se manejan con sintaxis de comprensión. Tomando el ejemplo anterior de Synesso :
// with potential nulls val father = if (person == null) null else person.father val mother = if (father == null) null else father.mother val sister = if (mother == null) null else mother.sister // with options val fathersMothersSister = for { father <- person.father mother <- father.mother sister <- mother.sister } yield sister
Si alguna de las asignaciones es
None
, elfathersMothersSister
seráNone
pero noNullPointerException
se levantará. A continuación, puede pasar de forma segurafathersMothersSister
a una función que toma parámetros de opción sin preocuparse. por lo que no verifica si hay nulos y no le importan las excepciones. Compare esto con la versión de Java presentada en el ejemplo de Synesso .fuente
<-
sintaxis se limite a la "sintaxis de comprensión de listas", ya que en realidad es la misma que lado
sintaxis más general de Haskell o ladomonad
forma de la biblioteca de mónadas de Clojure. Atarlo a listas lo vende corto.Tiene capacidades de composición bastante poderosas con Option:
def getURL : Option[URL] def getDefaultURL : Option[URL] val (host,port) = (getURL orElse getDefaultURL).map( url => (url.getHost,url.getPort) ).getOrElse( throw new IllegalStateException("No URL defined") )
fuente
Tal vez alguien más señaló esto, pero no lo vi:
Una ventaja de la coincidencia de patrones con Option [T] frente a la comprobación nula es que Option es una clase sellada, por lo que el compilador Scala emitirá una advertencia si no codifica el caso Some o None. Hay una bandera de compilador para el compilador que convertirá las advertencias en errores. Por lo tanto, es posible evitar la falla al manejar el caso "no existe" en tiempo de compilación en lugar de en tiempo de ejecución. Esta es una enorme ventaja sobre el uso del valor nulo.
fuente
No está ahí para ayudar a evitar una verificación nula, está ahí para forzar una verificación nula. El punto se vuelve claro cuando su clase tiene 10 campos, dos de los cuales podrían ser nulos. Y su sistema tiene otras 50 clases similares. En el mundo de Java, usted intenta prevenir NPE en esos campos usando alguna combinación de poder mental, convenciones de nomenclatura o incluso anotaciones. Y cada desarrollador de Java falla en esto en un grado significativo. La clase Option no solo hace que los valores "anulables" sean visualmente claros para cualquier desarrollador que intente comprender el código, sino que permite al compilador hacer cumplir este contrato previamente tácito.
fuente
[copiado de este comentario por Daniel Spiewak ]
fuente
Option
deNone
nuevo. Si las declaraciones se hubieran escrito como condicionales anidadas, cada "falla" potencial solo se probaría y se actuaría una vez. En su ejemplo, el resultado defetchRowById
se inspecciona efectivamente tres veces: una vez parakey
la inicialización de la guía , otra vez paravalue
's y finalmente pararesult
' s. Es una forma elegante de escribirlo, pero no deja de tener un costo de tiempo de ejecución.Un punto que nadie más parece haber planteado aquí es que, si bien puede tener una referencia nula, hay una distinción introducida por Option.
Eso es que puede tener
Option[Option[A]]
, que sería habitado porNone
,Some(None)
ySome(Some(a))
dondea
es uno de los habitantes habituales deA
. Esto significa que si tiene algún tipo de contenedor y desea poder almacenar punteros nulos en él y sacarlos, debe devolver algún valor booleano adicional para saber si realmente obtuvo un valor. Verrugas como esta abundan en las API de contenedores de Java y algunas variantes sin bloqueo ni siquiera pueden proporcionarlas.null
es una construcción única, no se compone de sí misma, solo está disponible para tipos de referencia y te obliga a razonar de una manera no total.Por ejemplo, cuando marca
if (x == null) ... else x.foo()
hay que llevar en la cabeza por toda la
else
rama esox != null
y eso ya ha sido comprobado. Sin embargo, al usar algo como la opciónx match { case None => ... case Some(y) => y.foo }
usted sabe que no es
None
por construcción, y sabría que tampoco lo fuenull
, si no fuera por el error de mil millones de dólares de Hoare .fuente
La opción [T] es una mónada, que es realmente útil cuando se utilizan funciones de orden superior para manipular valores.
Le sugiero que lea los artículos que se enumeran a continuación, son artículos realmente buenos que le muestran por qué la Opción [T] es útil y cómo puede usarse de manera funcional.
fuente
Además del adelanto de una respuesta de Randall , comprender por qué la ausencia potencial de un valor está representada por
Option
requiere comprender lo queOption
comparte con muchos otros tipos en Scala, específicamente, tipos de mónadas de modelado. Si uno representa la ausencia de un valor con nulo, esa distinción ausencia-presencia no puede participar en los contratos compartidos por los otros tipos monádicos.Si no sabe qué son las mónadas, o si no se da cuenta de cómo están representadas en la biblioteca de Scala, no verá con qué funciona
Option
y no podrá ver lo que se está perdiendo. Hay muchos beneficios de usar enOption
lugar de null que serían dignos de mención incluso en ausencia de cualquier concepto de mónada (analizo algunos de ellos en el hilo de la lista de correo de usuarios de Scala "Costo de la opción / Algunos vs null" aquí ), pero hablando de su aislamiento es como hablar sobre el tipo de iterador de una implementación de lista vinculada en particular, preguntarse por qué es necesario, mientras se pierde la interfaz más general de contenedor / iterador / algoritmo. Aquí también funciona una interfaz más amplia,Option
fuente
Creo que la clave se encuentra en la respuesta de Synesso: Option no es principalmente útil como un alias engorroso para null, sino como un objeto completo que luego puede ayudarlo con su lógica.
El problema con null es que es la falta de un objeto. No tiene métodos que puedan ayudarlo a lidiar con él (aunque como diseñador de lenguaje puede agregar listas cada vez más largas de características a su lenguaje que emulan un objeto si realmente lo desea).
Una cosa que Option puede hacer, como ha demostrado, es emular null; a continuación, debe probar el valor extraordinario "Ninguno" en lugar del valor extraordinario "nulo". Si lo olvidas, en cualquier caso, sucederán cosas malas. La opción hace que sea menos probable que suceda por accidente, ya que debe escribir "get" (lo que debería recordarle que podría ser nulo, es decir, Ninguno), pero este es un pequeño beneficio a cambio de un objeto contenedor adicional. .
Donde Option realmente comienza a mostrar su poder es ayudarlo a lidiar con el concepto de Yo-quería-algo-pero-en-realidad-no-tengo-uno.
Consideremos algunas cosas que podría querer hacer con cosas que podrían ser nulas.
Tal vez desee establecer un valor predeterminado si tiene un valor nulo. Comparemos Java y Scala:
String s = (input==null) ? "(undefined)" : input; val s = input getOrElse "(undefined)"
En lugar de una construcción?: Algo engorrosa, tenemos un método que se ocupa de la idea de "usar un valor predeterminado si soy nulo". Esto limpia un poco tu código.
Tal vez desee crear un nuevo objeto solo si tiene un valor real. Comparar:
File f = (filename==null) ? null : new File(filename); val f = filename map (new File(_))
Scala es un poco más corto y nuevamente evita fuentes de error. Luego, considere el beneficio acumulativo cuando necesite encadenar cosas como se muestra en los ejemplos de Synesso, Daniel y paradigmatic.
No es un vasto mejora, pero si agrega todo, vale la pena en todas partes, guarde el código de muy alto rendimiento (donde desea evitar incluso la pequeña sobrecarga de crear el objeto contenedor Some (x)).
El uso de coincidencias no es realmente tan útil por sí solo, excepto como un dispositivo para alertarlo sobre el caso nulo / Ninguno. Cuando es realmente útil es cuando comienzas a encadenarlo, por ejemplo, si tienes una lista de opciones:
val a = List(Some("Hi"),None,Some("Bye")); a match { case List(Some(x),_*) => println("We started with " + x) case _ => println("Nothing to start with.") }
Ahora puede plegar los casos None y los casos List-is-empty todos juntos en una declaración práctica que extrae exactamente el valor que desea.
fuente
Los valores de retorno nulos solo están presentes por compatibilidad con Java. No deberías usarlos de otra manera.
fuente
Realmente es una cuestión de estilo de programación. Usando Functional Java, o escribiendo sus propios métodos de ayuda, podría tener su funcionalidad Option pero no abandonar el lenguaje Java:
http://functionaljava.org/examples/#Option.bind
El hecho de que Scala lo incluya por defecto no lo hace especial. La mayoría de los aspectos de los lenguajes funcionales están disponibles en esa biblioteca y puede coexistir muy bien con otro código Java. Así como puede elegir programar Scala con nulos, puede elegir programar Java sin ellos.
fuente
Admitiendo de antemano que es una respuesta simplista, Option es una mónada.
fuente
De hecho, comparto la duda contigo. Acerca de Option, realmente me molesta que 1) haya una sobrecarga de rendimiento, ya que hay una gran cantidad de "Algunas" envolturas creadas en todas partes. 2) Tengo que usar mucho Some y Option en mi código.
Entonces, para ver las ventajas y desventajas de esta decisión de diseño de lenguaje, debemos considerar alternativas. Como Java simplemente ignora el problema de la nulabilidad, no es una alternativa. La alternativa real proporciona el lenguaje de programación Fantom. Hay tipos anulables y no anulables allí y?. ?: operadores en lugar del mapa / flatMap / getOrElse de Scala. Veo las siguientes viñetas en la comparación:
Ventaja de la opción:
Ventaja de Nullable:
Entonces no hay un ganador obvio aquí. Y una nota más. No hay una ventaja sintáctica principal para usar Option. Puede definir algo como:
def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)
O use algunas conversiones implícitas para obtener una sintaxis atractiva con puntos.
fuente
La ventaja real de tener tipos de opciones explícitos es que puede no usarlos en el 98% de todos los lugares y, por lo tanto, excluir estáticamente las excepciones nulas. (Y en el otro 2%, el sistema de tipos le recuerda que debe verificar correctamente cuándo accede a ellos).
fuente
Otra situación en la que Option funciona es en situaciones en las que los tipos no pueden tener un valor nulo. No es posible almacenar un valor nulo en un valor Int, Float, Double, etc., pero con una opción puede usar None.
En Java, necesitaría usar las versiones en caja (Integer, ...) de esos tipos.
fuente