¿Qué sentido tiene la clase Option [T]?

83

No puedo entender el sentido de la Option[T]clase en Scala. Quiero decir, no puedo ver ninguna ventaja de Nonemá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 getPerson1regresa null, luego la llamada realizada a displayen la primera línea de maines inevitable que falle NPE. De manera similar, si getPerson2regresa None, la displayllamada 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 nullo Noneen 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?

desaparecido faktor
fuente
23
En realidad, el método "get" en la Opción [T] se pronuncia: "¿Por qué diablos no coincide con este patrón?"
Mitch Blevins
2
Mitch tiene razón. Intente reformular su ejemplo sin usar gety lo obtendrá . :-)
Daniel C. Sobral
Tiene Person p .. que es java .. .try val p = ... Además, hay más en Option como lo muestran Daniel y Synesso a continuación - algunas GRANDES respuestas aquí.
Michael Neale
@Michael: ¡Ups! Gracias por señalar; lo corrigió.
missingfaktor

Respuestas:

72

Obtendrá el punto de Optionmejor si se obliga a nunca, nunca, usar get. Eso es porque getes el equivalente de "ok, envíame de vuelta a tierra nula".

Entonces, tome ese ejemplo suyo. ¿Cómo llamarías displaysin 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á recurrir displaya algo que no existe.

En cuanto a por qué getexiste, 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í:

Cuál es 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 repetirlo de otra manera: la principal ventaja de Option[T]over Tes la seguridad de tipos. Garantiza que no enviará un Tmé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 Optionque 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)
Daniel C. Sobral
fuente
5
"Oblígate a nunca, nunca, usar get" -> Entonces, en otras palabras: "¡No lo haces get!" :)
fredoverflow
31

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.

Synesso
fuente
En su último ejemplo, ¿qué pasa si el padre de la persona es nulo? mapvolverá Noney la llamada fallará con algún error. ¿Cómo es mejor que el nullenfoque?
missingfaktor
5
No. Si la persona es Ninguno (o padre, madre o hermana), entonces fathersMothersSister será Ninguno, pero no se arrojará ningún error.
paradigmático
6
Creo que te refieres a flatMap, en lugar de map.
retrónimo
Gracias por la edición Daniel. No probé el código antes de publicarlo. Lo haré mejor la próxima vez.
Synesso
2
val favouriteColour = if (p == null) p.favouriteColour else null // ¡precisamente el error que Option te ayuda a evitar! ¡Esta respuesta ha estado aquí durante años sin que nadie haya detectado este error!
Mark Lister
22

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 getPerson2obvia en virtud del hecho de que devuelve Option[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.

Michael Neale
fuente
1
Esta es mi comprensión de Option también. Le dice explícitamente al programador que podríamos obtener un Ninguno, y si eres lo suficientemente tonto como para recordar hacer Algo (T) pero no atrapar el Ninguno también, estás en problemas.
cflewis
1
Lewisham: creo que el compilador le dará una advertencia ya que Algunos / Ninguno forman un tipo de datos algebraicos (rasgo abstracto sellado ...) (pero voy de memoria aquí).
Michael Neale
6
El punto del tipo Option en la mayoría de los lenguajes que lo usan es que, en lugar de una excepción nula en tiempo de ejecución, obtiene un error de tipo de tiempo de compilación: el compilador puede saber que no tiene una acción para la condición None al usar los datos, que debería ser un error de tipo.
Justin Smith
15

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, el fathersMothersSisterserá Nonepero no NullPointerExceptionse levantará. A continuación, puede pasar de forma segura fathersMothersSistera 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 .

paradigmático
fuente
3
Es una pena que en Scala la <-sintaxis se limite a la "sintaxis de comprensión de listas", ya que en realidad es la misma que la dosintaxis más general de Haskell o la domonadforma de la biblioteca de mónadas de Clojure. Atarlo a listas lo vende corto.
seh
11
"Para comprensiones" en Scala son esencialmente el "hacer" en Haskell, no se limitan a listas, puedes usar cualquier cosa que implemente: def map [B] (f: A => B): C [B] def flatMap [B] (f: A => C [B]): C [B] filtro def (p: A => Booleano): C [A]. OIA, cualquier mónada
GClaramunt
2
@seh voté a favor del comentario de @ GClaramunt, pero no puedo enfatizar lo suficiente su punto. No hay conexión entre las comprensiones y las listas en Scala, excepto que la última se puede usar con la primera. Los remito a stackoverflow.com/questions/1052476/… .
Daniel C. Sobral
Sí, yo sé que no hay ninguna relación, pero estoy de acuerdo que vale la pena señalar; Estaba comentando en la primera línea de esta respuesta, donde paradigmático menciona "sintaxis de comprensión de listas". Es un problema de enseñanza, a diferencia de un problema de diseño de lenguaje.
seh
9

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") )
Viktor Klang
fuente
¿Podrías explicar esto completamente?
Jesvin Jose
8

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.

Paul Snively
fuente
7

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.

Adam Rabung
fuente
6

[copiado de este comentario por Daniel Spiewak ]

Si la única forma de usarlo Optionfuera la coincidencia de patrones para obtener valores, entonces sí, estoy de acuerdo en que no mejora en absoluto con null. Sin embargo, le falta una clase * enorme * de su funcionalidad. La única razón convincente para usar Optiones si está utilizando sus funciones de utilidad de orden superior. Efectivamente, debe utilizar su naturaleza monádica. Por ejemplo (asumiendo una cierta cantidad de recorte de 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 genial? De hecho, podemos hacerlo mucho mejor si usamos for-comprehensions:

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 * estamos verificando explícitamente los valores nulos, Ninguno o cualquiera de su tipo. El objetivo de Option es evitar esas comprobaciones. Simplemente encadena 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 realizar una verificación explícita (que nunca debería tener que hacer), proporcionar un valor predeterminado, lanzar una excepción, etc.

Nunca, nunca hago ninguna comparación explícita contra 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 en Option(o Box, en el caso de Lift) como una señal de que el desarrollador que escribió el código no comprende completamente el lenguaje y su biblioteca estándar.

No me refiero a ser un martillo troll, pero realmente necesitas ver cómo las características del lenguaje se utilizan * realmente * en la práctica antes de tacharlas como inútiles. Estoy absolutamente de acuerdo en que Option es bastante poco convincente como * tú * lo usaste, pero no lo estás usando de la forma en que fue diseñado.

desaparecido faktor
fuente
Aquí hay una triste consecuencia: no hay un cortocircuito basado en saltos en juego, por lo que cada declaración sucesiva prueba el Optionde Nonenuevo. 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 de fetchRowByIdse inspecciona efectivamente tres veces: una vez para keyla inicialización de la guía , otra vez para value's y finalmente para result' s. Es una forma elegante de escribirlo, pero no deja de tener un costo de tiempo de ejecución.
seh
4
Creo que malinterpreta las comprensiones de Scala. El segundo ejemplo NO es enfáticamente un bucle, el compilador lo traduce en una serie de operaciones flatMap, como en el primer ejemplo.
Kevin Wright
Ha pasado mucho tiempo desde que escribí mi comentario aquí, pero acabo de ver el de Kevin. Kevin, ¿a quién te refieres cuando escribiste "no entiendes?" No veo cómo podría haber sido yo , ya que nunca mencioné nada sobre un bucle.
seh
6

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 por None, Some(None)y Some(Some(a))donde aes 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, 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 elserama eso x != nully eso ya ha sido comprobado. Sin embargo, al usar algo como la opción

x match {
case None => ...
case Some(y) => y.foo
}

usted sabe que no es Nonepor construcción, y sabría que tampoco lo fue null, si no fuera por el error de mil millones de dólares de Hoare .

Edward KMETT
fuente
3

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.

Brian Hsu
fuente
Agregaré a la lista de lectura recomendada el tutorial recientemente publicado de Tony Morris "¿Qué significa Monad?": Projects.tmorris.net/public/what-does-monad-mean/artifacts/1.0/…
Randall Schulz
3

Además del adelanto de una respuesta de Randall , comprender por qué la ausencia potencial de un valor está representada por Optionrequiere comprender lo que Optioncomparte 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 Optiony no podrá ver lo que se está perdiendo. Hay muchos beneficios de usar en Optionlugar 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

seh
fuente
Muchas gracias por el enlace. Fue realmente útil. :)
missingfaktor
Tu comentario sobre el hilo fue tan conciso que casi pierdo el sentido. Realmente deseo que se pueda prohibir null.
Alain O'Dea
2

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.

Rex Kerr
fuente
2

Los valores de retorno nulos solo están presentes por compatibilidad con Java. No deberías usarlos de otra manera.

centeno
fuente
1

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.

Sam Pullara
fuente
0

Admitiendo de antemano que es una respuesta simplista, Option es una mónada.

Randall Schulz
fuente
Sé que es una mónada. ¿Por qué más debería incluir una etiqueta "mónada" en cuestión?
missingfaktor
^ La declaración anterior no significa que yo entienda qué es una mónada. : D
missingfaktor
4
Las mónadas son geniales. Si no los usa o al menos no finge entender, entonces no está bien ;-)
paradigmático
0

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:

  1. lenguaje más simple: no se requieren construcciones de lenguaje adicionales
  2. uniforme con otros tipos monádicos

Ventaja de Nullable:

  1. sintaxis más corta en casos típicos
  2. mejor rendimiento (ya que no necesita crear nuevos objetos Option y lambdas para map, flatMap)

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.

Alexey
fuente
¿Alguien ha realizado pruebas comparativas sólidas sobre el impacto del rendimiento en una máquina virtual moderna? El análisis de escape significa que muchos objetos Option temporales se pueden asignar en la pila (mucho más barato que el montón), y el GC generacional maneja objetos un poco menos temporales de manera bastante eficiente. Por supuesto, si la velocidad es más importante para su proyecto que evitar las NPE, es probable que las opciones no sean para usted.
Justin W
No mencione la sobrecarga de rendimiento sin números para respaldarla. Este es un error extremadamente común cuando se argumenta en contra de abstracciones como Option. Con mucho gusto revertiré mi voto negativo si señala o publica un punto de referencia o elimina el comentario de rendimiento :)
Alain O'Dea
0

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

Andreas Rossberg
fuente
-3

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.

Arne
fuente