settitle - titledborder java
¿Cuál es el punto de la clase Opción[T]? (18)
No puedo entender el punto de la clase Option[T]
en Scala. Quiero decir, no puedo ver ninguna advanages de None
sobre 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 supongamos que el método getPerson1
devuelve null
, y luego la llamada realizada para display
en la primera línea de main
está destinada a fallar con NPE
. De forma similar, si getPerson2
devuelve None
, la llamada a la display
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 particular de la Option[T]
. Tengo que probar el null
excepcional o None
en ambos casos. :(
Si entendí correctamente la respuesta de @Michael , ¿la única ventaja de la Option[T]
es que explícitamente le dice 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?
Admitiendo de antemano que es una respuesta simplista, Option es una mónada.
Agregando al teaser de una respuesta de Randall, entender por qué la ausencia potencial de un valor está representada por la Option
requiere la comprensión de qué Option
comparte con muchos otros tipos en Scala específicamente, tipos que modelan mónadas. 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 nota cómo están representadas en la biblioteca de Scala, no verá qué Option
juega junto con, y no puede ver lo que se está perdiendo en . Hay muchos beneficios al usar Option
lugar de null que serían notables incluso en ausencia de cualquier concepto de mónada (analizo algunos de ellos en el hilo de la lista de correo de Scala-User de "Cost of Option / Some vs null" here ), pero hablando al respecto, el aislamiento es algo así como hablar sobre el tipo de iterador de la implementación de una lista enlazada particular, preguntándose 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, y Option
proporciona un modelo de presencia y ausencia de esa interfaz.
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)
El enlace de propiedad monádica, que aparece en Scala como la función de mapa , nos permite encadenar operaciones en objetos sin preocuparse de si son ''nulos'' o no.
Toma este simple ejemplo un poco más. 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.
Creo que la clave se encuentra en la respuesta de Synesso: la opción no es principalmente útil como un alias engorroso para null, sino como un objeto completo que puede ayudarte con tu lógica.
El problema con null es que es la falta de un objeto. No tiene métodos que puedan ayudarlo a lidiar con eso (aunque como diseñador de lenguaje puede agregar listas cada vez más largas de características a su idioma que emulan un objeto si realmente lo desea).
Una cosa que la Opción puede hacer, como lo has demostrado, es emular el nulo; luego debe probar el valor extraordinario "Ninguno" en lugar del valor extraordinario "nulo". Si lo olvida, en cualquier caso, sucederán cosas malas. La opción hace que sea menos probable que ocurra por accidente, ya que tienes que escribir "get" (lo que debería recordarte que podría ser nulo, es decir, None), pero este es un pequeño beneficio a cambio de un objeto contenedor adicional .
Donde Option realmente empieza a mostrar su poder te ayuda a lidiar con el concepto de I-wanted-something-but-I-don''t-actually-have-one.
Consideremos algunas cosas que podría querer hacer con cosas que podrían ser nulas.
Tal vez quiera 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 un tanto engorrosa?: Tenemos un método que trata con la idea de "usar un valor predeterminado si soy nulo". Esto limpia tu código un poco.
Tal vez quiera 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 las fuentes de error. Luego, considere el beneficio acumulativo cuando necesite encadenar las cosas como se muestra en los ejemplos de Synesso, Daniel y paradigmático.
No es una gran mejora, pero si suma todo, vale la pena en todas partes, salvo el código de alto rendimiento (donde desea evitar incluso la pequeña sobrecarga de crear el objeto contenedor Some (x)).
El uso del partido no es tan útil por sí mismo, 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 doblar los casos Ninguno y List-is-empty juntos en una sola y práctica declaración que extrae exactamente el valor que desea.
En realidad, comparto la duda contigo. Acerca de la opción, realmente me molesta que 1) haya una sobrecarga de rendimiento, ya que existe una gran cantidad de envolturas "Algunas" creadas en cada momento. 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 del 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 de nulos y nulos aquí y? ?: operadores en lugar del mapa de Scala / flatMap / getOrElse. Veo las siguientes viñetas en la comparación:
La ventaja de la opción:
- lenguaje más simple - no se requieren construcciones de lenguaje adicionales
- uniforme con otros tipos monádicos
Ventaja de Nullable:
- sintaxis más corta en casos típicos
- un mejor rendimiento (ya que no es necesario crear nuevos objetos Opción y lambdas para el mapa, flatMap)
Entonces no hay un ganador obvio aquí. Y una nota más. No hay 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 sintaxis de pritty con puntos.
Es realmente una pregunta de estilo de programación. Usando Functional Java, o escribiendo sus propios métodos de ayuda, podría tener su funcionalidad de Opción pero no abandonar el lenguaje de Java:
http://functionaljava.org/examples/#Option.bind
El hecho de que Scala lo incluya de manera predeterminada no lo hace especial. La mayoría de los aspectos de los lenguajes funcionales están disponibles en esa biblioteca y pueden coexistir muy bien con otros códigos Java. Del mismo modo que puede elegir programar Scala con nulos, puede optar por programar Java sin ellos.
La diferencia es sutil. Tenga en cuenta que para ser verdaderamente una función, debe devolver un valor: nulo no se considera realmente como un "valor de retorno normal" en ese sentido, más como un tipo de fondo / 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, puedes hacer algo similar con nulo, pero esto hace obvia la semántica de llamar a getPerson2
en virtud del hecho de que devuelve Option[Person]
(una práctica práctica, aparte de confiar en que alguien lea el documento y obtenga un NPE porque no lea el documento).
Intentaré desenterrar a un programador funcional que pueda dar una respuesta más estricta que yo.
La opción [T] es una mónada, que es realmente útil cuando se usan funciones de orden alto para manipular valores.
Le sugiero que lea los artículos que figuran a continuación, son realmente buenos artículos que le muestran por qué la opción [T] es útil y cómo puede usarse de manera funcional.
La ventaja real de tener tipos de opciones explícitos es que no puede usarlos en el 98% de todos los lugares y, por lo tanto, excluye estáticamente excepciones nulas. (Y en el otro 2%, el sistema de tipo le recuerda que debe verificar correctamente cuando realmente accede a ellos).
Los valores de retorno nulos solo están presentes para la compatibilidad con Java. No deberías usarlos de otra manera.
No está ahí para ayudar a evitar una verificación nula, está ahí para forzar una verificación nula. El punto queda 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, intenta evitar NPE en esos campos usando alguna combinación de poder mental hores, convención de nombres 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 los desarrolladores que intentan entender el código, sino que permite que el compilador aplique este contrato previamente no hablado.
Obtendrá mejor el punto de la Option
si se obliga a nunca, nunca, a usar get
. Eso es porque get
es el equivalente a "ok, enviarme de regreso a null-land".
Entonces, toma ese ejemplo tuyo. ¿Cómo llamarías a la display
sin usar get
? 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á llamar a display
algo que no existe.
En cuanto a por qué get
existe, Scala no te dice cómo debe escribirse tu código. Puede estimularlo suavemente, pero si quiere recurrir a una red de seguridad, es su elección.
Lo has clavado aquí:
¿La única ventaja de la Opción [T] es que le dice explícitamente al programador que este método podría devolver None?
Excepto por el "único". Pero permítanme exponerlo de otra manera: la principal ventaja de la Option[T]
sobre T
es la seguridad del tipo. Asegura que no enviará un método T
a un objeto que puede no existir, ya que el compilador no lo permitirá.
Dijiste que tienes que probar la nulidad en ambos casos, pero si te olvidas, o no sabes, tienes que comprobar si es nulo, ¿te lo dirá el compilador? ¿O serán tus usuarios?
Por supuesto, debido a su interoperabilidad con Java, Scala permite nulos al igual que Java. Por lo tanto, si usa bibliotecas Java, si usa bibliotecas Scala mal escritas, o si usa bibliotecas Scala personales mal escritas, igual tendrá que lidiar con punteros nulos.
Otras dos ventajas importantes de la Option
I pueden ser:
Documentación: una firma de tipo de método le dirá si un objeto siempre se devuelve o no.
Compilabilidad monádica
Esta última toma mucho más tiempo para apreciarse completamente, y no es adecuada para ejemplos simples, ya que solo muestra su fortaleza en códigos complejos. Por lo tanto, daré un ejemplo a continuación, pero soy consciente de que apenas significará nada, excepto para las personas que ya lo tienen.
for {
person <- getUsers
email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)
Otra situación donde Option funciona, es en situaciones donde los tipos no pueden tener un valor nulo. No es posible almacenar nulo en un valor Int, Flotante, Doble, etc., pero con una Opción puede usar Ninguno.
En Java, necesitarías usar las versiones en caja (Entero, ...) de esos tipos.
Para mí, las opciones son realmente interesantes cuando se manejan con la 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 son None
, la fathersMothersSister
será None
pero no se generará NullPointerException
. A continuación, puede pasar de forma segura a la fathersMothersSister
del fathersMothersSister
a una función que tome los parámetros de la opción sin preocuparse. por lo que no verifica nulo y no le importan las excepciones. Compare esto con la versión de Java presentada en el ejemplo de Synesso .
Tal vez alguien más señaló esto, pero yo no lo vi:
Una ventaja de la coincidencia de patrones con la opción [T] frente a la comprobación nula es que Option es una clase sellada, por lo que el compilador de Scala emitirá una advertencia si no codifica el caso Some o None. Hay un indicador de compilación para el compilador que convertirá las advertencias en errores. Por lo tanto, es posible evitar que la falla al manejar el caso "no existe" en tiempo de compilación en lugar de en tiempo de ejecución. Esta es una gran ventaja sobre el uso del valor nulo.
Un punto que nadie más parece haber planteado es que si bien puede tener una referencia nula, Option introduce una distinción.
Es decir, puede tener la Option[Option[A]]
, que estaría habitada por None
, Some(None)
y Some(Some(a))
donde a
es uno de los habitantes habituales de A
Esto significa que si tiene algún tipo de contenedor y desea poder almacenar punteros nulos en él y sacarlos, necesita devolver algún valor booleano adicional para saber si realmente obtuvo un valor. Verrugas como esta abundan en las API de los contenedores java y algunas variantes sin candado ni siquiera pueden proporcionarlas.
null
es una construcción única, no se compone sola, solo está disponible para tipos de referencia y te obliga a razonar de una manera no total.
Por ejemplo, cuando controlas
if (x == null) ...
else x.foo()
tienes que llevar en tu cabeza la rama else
que x != null
y que esto ya ha sido verificado. Sin embargo, cuando se usa algo como la opción
x match {
case None => ...
case Some(y) => y.foo
}
usted sabe que no es None
por construcción, y usted sabría que tampoco era null
, si no fuera por el error de mil millones de dólares de Hoare.
Usted tiene capacidades de composición bastante poderosas con la opción:
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") )
[copiado de este comentario por Daniel Spiewak ]
Si la única forma de utilizar
Option
fuera la coincidencia de patrones para obtener valores, entonces sí, estoy de acuerdo en que no mejora en absoluto por nulo. Sin embargo, te falta una clase * enorme * de su funcionalidad. La única razón de peso para usarOption
es si está utilizando sus funciones de utilidad de orden superior. Efectivamente, necesitas usar su naturaleza monádica. Por ejemplo (suponiendo una cierta cantidad de recorte API):
val row: Option[Row] = database fetchRowById 42 val key: Option[String] = row flatMap { _ get “port_key” } val value: Option[MyType] = key flatMap (myMap get) val result: MyType = value getOrElse defaultValue
Ahí, ¿no fue eso ingenioso? Realmente podemos mejorar mucho si usamos
for
-comprehensiones:
val value = for { row <- database fetchRowById 42 key <- row get "port_key" value <- myMap get key } yield value val result = value getOrElse defaultValue
Notarás que * nunca * comprobamos explícitamente null, None o cualquiera de su índole. El objetivo de Opción es evitar cualquiera de esas comprobaciones. Simplemente encierra los cálculos y avanza por la línea hasta que * realmente * necesite obtener un valor. En ese punto, puede decidir si desea o no hacer una comprobación explícita (que nunca debería tener que hacer), proporcionar un valor predeterminado, lanzar una excepción, etc.
Nunca, nunca hago ninguna coincidencia explícita con
Option
, y conozco a muchos otros desarrolladores de Scala que están en el mismo barco. David Pollak me mencionó el otro día que usa esa coincidencia explícita enOption
(oBox
, en el caso de Lift) como una señal de que el desarrollador que escribió el código no comprende completamente el idioma y su biblioteca estándar.No me refiero a ser un troll hammer, pero realmente necesitas ver cómo las características del lenguaje * realmente * se usan en la práctica antes de considerarlas inútiles. Estoy absolutamente de acuerdo en que Option es bastante poco atractiva ya que * you * lo usó, pero no lo está usando de la forma en que fue diseñado.