Hola Ceylon, adiós NullPointerException

Todo programador que haya hecho aunque sea la más pequeña aplicación en Java, se ha topado en algún momento con la odiosa NullPointerException. Una excepción que sólo ocurre en tiempo de ejecución, cuando resulta que nuestro código invoca un método o quiere obtener un miembro de un objeto que realmente apunta a null. Y puede ocurrir por cosas tan simples como esto:

String x = "hola";
//algunas líneas más abajo...
if (cualquierCosa) { hola = null; }
//y otras líneas más abajo
int largo = x.length();

El código compila perfectamente. Es más, esto es código que compila sin darnos ningún error, ni advertencia, ni nada:

String x = null;
x.length();

Y uno se pone a pensar... es tan difícil que el compilador se diera cuenta que tengo una variable que apunta a null, y en la siguiente línea estoy invocando un método sobre ese null??????; pero pues así funciona Java. Y si lo anterior es posible, por supuesto puede ocurrir dentro de cualquiera de nuestros métodos que esperan recibir un objeto.

A veces hay que validar si nuestro método recibe realmente un objeto para no nada más arrojar NPE sino algún otro tipo de excepción como InvalidArgumentException, pero pues tampoco es práctico tener un if (x == null) cada 3 líneas de código. Pero a veces lo llegamos a ver... este es uno de los problemas por los que hay tantos errores en Java y por los que luego se infla tanto el código.

Las herramientas de análisis estático por detectan muchas de estas situaciones, pero no todo mundo las usa, algunos ni siquiera las conocen; y quienes las usan, pues no las usan todo el tiempo, pueden llegar a dejar alguna parte del código fuera del analizador. En fin. Problemas y más problemas.

La solución a los problemas de null

En Ceylon, null es un objeto. Y por lo tanto tiene tipo: es de tipo Nothing. No es una palabra reservada del lenguaje, ni una referencia mágica a la que cualquier objeto puede apuntar para indicar que realmente no apunta a nada.

El hecho que null sea un objeto y tenga tipo, junto con las características que ya he mencionado antes de unión e intersección de tipos, nos permite escribir código libre de estos problemas.

Por ejemplo, este código no compila en Ceylon:

String x = null;

El error que arroja el compilador es este:

specified expression must be assignable to declared type: Nothing is not assignable to String

Así de simple. En un lenguaje de tipado estático, no podemos declarar un valor tipo String y luego asignarle un objeto de otro tipo (tipo Nothing).

Del mismo modo, si tenemos una función o método definida así:

String reverse(String s);

No podemos invocar reverse(null) porque el compilador nos arroja un error similar: la función espera un parámetro tipo String y le estamos pasando un objeto tipo Nothing. Esto significa que puedo implementar mi función reverse sin tener que preocuparme de que alguien le pueda pasar un null, porque simplemente no es posible, es un error que se detecta en tiempo de compilación. Del mismo modo, por estar declarando que reverse devuelve un String, si al implementarla quiero poner un return null en alguna parte, no compila, el compilador me indica que no puedo devolver un objeto tipo Nothing si la función declara que devuelve un String. Y por lo mismo, quien sea que use mi función podrá estar 100% seguro de que nunca le va a devolver null. No hay preocupación, no es necesario ni siquiera contemplar ese caso, sobra poner un if (null).

Por supuesto, si por alguna razón necesitamos poder asignar null a un valor o recibirlo como parámetro o poder usarlo como valor de retorno, la unión de tipos nos permitirá hacerlo. Si queremos redefinir reverse para tener los mismos problemas que tendríamos en Java, entonces:

String|Nothing reverse(String|Nothing s)

Y bueno, para no escribir tanto, existe una sintaxis más breve. De hecho cuando se hace unión de un tipo con Nothing, se dice que es un tipo (o valor) opcional:

String? reverse(String? s)

El sufijo ? en una declaración de tipos es equivalente a poner |Nothing. Pero entonces ocurre algo: para poder usar un valor opcional, el compilador nos exige que primero nos aseguremos de que el valor existe (o no existe). Es decir, este código no compila:

String? x = null;
print(x.size);

El error que arroja el compilador:

member method or attribute does not exist: size in type Nothing|String

Es decir, dado que tenemos una unión de tipos, para poder invocar los métodos de un String, primero hay que asegurarnos de que es un String. Ceylon tiene el operador exists para indicarnos si un objeto es de tipo Nothing o no, y eso es lo que debemos usar cuando tenemos valores opcionales:

String? x = null;
if (exists x) {
  print(x.size);
} else {
  print("No existe x");
}

Este código ya compila. Por lo tanto, cuando manejamos tipos opcionales, sí debemos forzosamente manejar la posibilidad de que sean null; pero todo esto nos lleva a cambiar el diseño de nuestros objetos/componentes/bibliotecas/software en general, porque únicamente vamos a declarar como opcional lo que realmente debe ser opcional. Por ejemplo, si tenemos la definición de reverse(String?) entonces podemos hacer esto:

String? x = null;
reverse(x);

Pero si regresamos a la definición de reverse(String) entonces tenemos que invocarlo así:

String? x = null;
if (exists x) {
  reverse(x); //Aquí dentro, x ya es tipo String
}
//Otra manera
reverse(x?""); //si no existe x, pasa ""
//Otra
reverse(x else ""); //lo mismo

Hay algunos otros operadores para lidiar con los tipos opcionales, pero esto lo publicaré después, cuando hable de los operadores de Ceylon en general. Por ahora quise enfocarme a presentar el concepto de un null con seguridad de tipo.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Supongo que lo siguiente

Supongo que lo siguiente se contestará en post siguientes, pero la pregunta obligada es, qué referencia tienen por default los objetos cuando aún no han sido inicializados?

ejemplo

shared class Persona {

     String name;
     String lastName;

}

Supongo que la respuesta será , pues forzarlos a tener un valor inicial ( como pasa con los valores marcados como final en Java donde el atributo debe de tener un valor inicial o serle pasado en el constructor )

El uso del safe operator ( no sé como se llame en Ceylon ) se ha vuelto cada vez más popular, ¿como difiere por ejemplo con el de Groovy o con el de Fantom ( que también es de tipeo estático ) o el de Gosu? Anteriormente se mencionó también en Scala, pero ahí en vez de tener un operador tenemos ahí la magia viene en que no es necesario hacer el cast que se hace acá ¿Que ventajas tiene uno y otro?

Finalmente y pregunta obligada, ¿como va a funcionar con la interoperabilidad con Java? es decir, los métodos y variables en Java si pueden ser nulos y puede ser que ahí hay que torcerle un poco la mano a Ceylán para que "deje" pasar esos usos. Pero como va a funcionar esto?

Muy interesante.

Saludos

Imagen de bferro

Programación defensiva

Quitarse de encima el NullPointerException es buena idea, pero no usaría yo el ejemplo que pones en Java:

String x =null;
x.length();

para comentar sobre los problemas que en Java se tiene con apuntadores nulos. Creo que eso nadie lo escribiría aunque el código compile.
El ejemplo de Ceylon:

void hello(String? name) {
   if (exists name)
      print("Hello,  " name "!");
   else
       print ("Hello World");
}

tiene la misma semántica ( y una sintaxis "similar") que el código Java:

void hello(String name) {
   if (name!=null)
      print("Hello,  " +name+ "!");
   else
       print ("Hello World");
}

En ambos casos, el programador debe defenderse de una condición excepcional, por lo que no habría mucha diferencia entre los dos lenguajes.

Lo que me gusta en Ceylon, y comparto esa idea, es el tratamiento uniforme para null como un objeto con un tipo asociado. De esta forma puedo hacer cosas uniformes como con el resto de los objetos, entre ellas las de poder decidir en tiempo de diseño a qué objeto se le puede asignar la referencia null.

Imagen de ezamudio

Inicialización

Hoy día esto compila bien:

shared class Persona() {
  String name;
  String lastName;
}

Es una clase con dos atributos inmutables indefinidos. Como nadie los intenta usar, no hay bronca; el código compila bien. Pero si agregas cualquier código que lo quiera utilizar (dentro de la clase en este caso, dado que no son compartidos):

shared class Persona() {
  String name;
  String lastName;
  shared actual String string = "Persona " name " " lastName "";
}

Entonces el compilador ya te arroja un error (bueno 2), que dicen not definitely specified: name (y lo mismo para lastName).

Si quitamos el `string` pero hacemos compartidos los atributos, tampoco compila:

shared class Persona() {
  shared String name;
  shared String lastName;
}

El compilador arroja dos errores must be definitely specified by class initializer. Si lo piensas, el problema no está en declarar valores inmutables indefinidos, sino en usarlos. Incluso en variables. Este último error del compilador ocurre incluso si haces variables los atributos, es decir shared variable String name. El primer caso compila porque son atributos privados y nadie los usa; el segundo truena porque ya intentamos usarlos; el tercero y cuarto casos (cuando ya los hacemos públicos) truenan porque son susceptibles de ser usados fuera de la clase y entonces se hacen verificaciones más estrictas.

El operador nullsafe funciona bastante similar al de Groovy, hasta ahora no le veo diferencia. Estamos hablando de a?.b o a?.b(); el operador para default en Ceylon de a?b es similar al Elvis de Groovy a?:b, la diferencia principal está en lo que Groovy considera para cumplir la condición, que incluye null, cadenas vacías, colecciones vacías y números que valgan 0 o 0.0.

No conozco Fantom ni Gosu como para decirte la diferencia con el operador equivalente en esos lenguajes. Y en Scala que yo recuerde no hay un operador nullsafe, no sé a cuál magia te refieres, ni tampoco sé a cuál cast te refieres porque el código Ceylon que puse no tiene ningún cast, pues no hay cast en Ceylon de hecho; si te refieres al tipo de x dentro del bloque de if (exists x), lo que ocurre ahí es que se refina el tipo de x, lo que realmente ocurre es que se le quita el Nothing a la unión, pero no se hace cast a String. Ya no quise complicar más ese asunto porque es una intro al manejo de nulos en Ceylon, pero la cosa es que si en vez de String? tuvieras algo como String|Integer?, entonces dentro del bloque, x sería String|Integer y todavía tendrías que hacer un switch o una serie de if's para refinar el tipo hasta saber qué cosa es x).

Imagen de ezamudio

Re: Programación defensiva

La gran diferencia entre el código Java y el código Ceylon en ese ejemplo, es que en Java no hay manera de saber si se va a recibir un null, por eso menciono al principio que la única opción es llenar todo el código de if (x!=null), para cada objeto que pueda ser null. En Ceylon sólo es necesario cuando el tipo del objeto incluye Nothing.

Imagen de ezamudio

Interop

En cuanto a la interoperabilidad con Java, pues sí, ahí no hay más que dos opciones: considerar todos los tipos del código Java como opcionales, o no considerarlos opcionales y simplemente aceptar el hecho de que en tiempo de ejecución el código Java puede causar una NPE.

Desconozco por el momento cuál será la opción que se tome pues interop es algo en lo que ya se está trabajando pero no está terminado y se han encontrado con algunos obstáculos imprevistos que a veces requieren modificar la estrategia para tener la mejor interop posible.

Imagen de benek

Bastante bien el Nothing, me

Bastante bien el Nothing, me recordó al Some y None de Scala pero generalizado a cualquier valor de tipo null.

Imagen de ezamudio

Option

El Option de Scala es un parche para darle la vuelta al hecho de que en Scala sí hay null y puedes tener los mismos problemas que en Java si no eres cuidadoso.

Imagen de Shadonwk

Muy buen post, espero los

Muy buen post, espero los proximos +1

Imagen de bferro

null es un objeto de tipo scala.Null

Efectivamente, en Scala existe el valor null, que a diferencia de Java, SÍ se tipifica con la clase scala.Null. Esa clase, de conjunto con la clase scala.Nothing es lo que Scala llama sus bottom types y ambas son subclases de cualquier clase de tipo AnyRef. Esa es la razón por la que puedo asignar a cualquier referencia el valor null
.
Cuando digo que no es un parche, lo que quiero decir es que al trabajar con Option[...] acepto el principio de que nunca voy a usar null para indicar que no hay valor, y en su lugar voy entonces a usar None, evitando muchos problemas (no todos).
Y si lo tomamos como parche, tampoco importa: Apache Server := A patchy Server

Imagen de ezamudio

Option es un contrato

Entonces podemos decir que Option es un contrato. Lo recomendable por supuesto es utilizarlo (incluso escribí hace tiempo de las ventajas de su uso en Scala); que el código propio no devuelva null sino que siempre se devuelva un Option. En Scala, un método que devuelva null se puede considerar un code smell.

La idea en Ceylon es ser más estricto con los nulos: simplemente si un parámetro, variable, valor de retorno, etc no incluye unión con Nothing, entonces tenemos la tranquilidad de no tener que checar si ese valor es nulo. Y en el caso contrario, cuando hay unión con Nothing (es un tipo opcional), forzosamente hay que checar si el valor existe (no es null).

Option es un monad

Imagen de bferro

Me gusta llamarlo asi

Me gusta como dices. Es efectivamente un contrato.
Por supuesto que como dije anteriormente, la idea de Ceylon de tratar a los nulos como lo hace es muy buena. Queda más claro en el diseño quien puede ser nulo y eso gracias a los tipos unión (otra muy buena idea) y otras cosas.

Imagen de ezamudio

Monad

Oscar, estamos hablando desde un punto de vista más... filosófico, por así decirlo; digo que Option es un contrato porque es algo que decides usar en tu código, pero bien podría valerte gorro y que devuelvas (y manejes) null por todos lados. Pero son útiles las ligas para quienes quieran ver lo son las mónadas y otras monadas de la programación funcional.

Imagen de bferro

Buenos papers sobre monads

Esta liga http://homepages.inf.ed.ac.uk/wadler/topics/monads.html tiene artículos excelentes sobre monads

Imagen de beto.bateria

¿Hola Ceylon, adiós NullPointerException?

Xiale, es la misma puerca pero revolcada, creo que lo mejor es hacer un buen analisis y diseño para evitarnos esos problemas.

Y si manejamos OOP, null es un valor en la vida real, ¿para que lo quieren evitar?

Un voto por la sencilles en todo.

@beto.bateríaSi y no. Hubo

@beto.batería

Si y no.

Hubo algún tiempo en el que los lenguajes de programación no tenían la noción de null ( estaba tratando de buscar la referencia pero no la encontré ) luego fue introducida y se popularizó tanto que ahora es "natural"

En realidad sí es relativamente fácil evitar Npe, sin tener que plagar el código con

if ( x != null ) {
   blah();
}

y cosas así.

Lo que hay que hacer es tener control de la forma en la que entran los valores al estado del objeto y como se modifican ( ja casi nada no? )

A lo que me refiero es a lo siguiente, en ves de tener un código con digamos 10 metodos y en cada uno poner e:

if ( solicitante != null ) {
   solicitante.blah();
}

Basta con hacer la variable solicitante privada y no crear metodos que la modifiquen, asegurarse que tenga un valor inicial al crear el objeto y así todos los métodos lo que lo usen "confiarán" que está en buen estado:

class Solicitud {
    private final Solicitante solicitante;
    Solicitud( Solicitante s ) {
        if ( s == null ) { throw new NullPointerException("El solicitante no puede ser nulo"); }
        this.solicitante = s;
    }
    public void usoUno() {
        solicitante.blah(); // no null check
    }
    public void usoDos() {
        solicitante.blop(); // no null check
    }
   ...
}

Etc.

Ahora, esta en una técnica muy sencilla, pero a juzgar por la graaaaan cantidad de NPE que salen todos los dias en todo el mundo a todos los programadores ( más a los principiantes ) parece un mal generalizado e innecesario. Si el lenguaje de programación provee un mecanismo estándar desde el diseño para solucionarlo yo creo que es bienvenido.

Lo único malo es que al tener que soportar los dos esquema ( Ceylon + Java Interop ) quedará irremediablemente en algún punto intermedio por ahí.

Pero bueno, saber si va a funcionar o no, por ahora es un tanto prematuro, pero sin duda alguna útil conocer.

Saludos.

@OscarRyzSi, requerimos que

@OscarRyz

Si, requerimos que especifiques un valor inicial para todo atributo.

http://ceylon-lang.org/documentation/tour/initialization/

Yo creo que nuestro sistema que utiliza un tipo union para representar la opcionalidad es mas conveniente y elegante que un objeto "wrapper" como se utiliza en ML, Haskell, o Scala. En esos lenguajes hay que meter y sacar tu valor de otro objecto. En Ceylon no se necesita. Ademas, en el entorno, no existe el tipo Nothing (es borrado por el compilador), y el null de Ceylon es el mismo null del JVM. En Scala, siempre tienes el wrapper en el entorno.

Para la interoperabilidad, aceptamos que en el codigo de Java no tenemos informacion sobre opcionalidad. Entonces te dejamos escribir:

String name = person.name;

o

String? name = person.name;

En caso de que hayas escrito String y getName() resulte en null, tendras un NullPointerException. Yo lo veo como algo normal. Estas interoperando con Java. Java no tiene seguridad para los valores nulos. No quiero hacerte escribir siempre if (exists ...) cada vez que llamas a Java.

Imagen de bferro

Null references: The Billion dollar mistake

La referencia al origen de null que OscarRyz busca es ésta:
Tony Hoare (con todo respeto Sir Charles Antony Richard Hoare, uno de los grandes) introdujo las referencias nulas en ALGOL W en el año 1965, argumentando las facilidades para su implementación.
Hoare lo bautizó más adelante como "el Error del billón de dólares".

Tipo inicial

respecto a "requerimos que especifiques un valor inicial para todo atributo."
¿no seria mejor tener un default definido para cada tipo, como hace c#? Para los numeros, 0, para los Strings, cadenas vacias...

Mas de una cosa de Ceylon me recuerda a Nice (http://nice.sourceforge.net/), un viejo lenguaje para la jvm que en su época era muy interesante...después quedó en la nada, desgraciadamente.

Imagen de ezamudio

cuál sería el punto?

Y cuál sería el punto de tener atributos con cero y cadenas vacías? Recuerda que muchos de estos atributos son inmutables, a menos que los marques como variables. E incluso cuando son variables requieren valor inicial, para evitar problemas de variables no inicializadas.

Para variables parecería que tiene sentido que se usen defaults, el problema es que no todos los tipos tienen un default que tenga sentido. Para enteros es obvio usar 0, para Strings es obvio usar cadena vacía. Para fechas, cuál sería el default obvio? obvio para quién? Cuál debe ser el default para un tipo unión? Por ejemplo si tienes Integer|String, qué default debe tener, 0 o cadena vacía?

Para evitar todas estas confusiones, mejor el programador le pone el valor inicial que tenga sentido para cada caso.

Imagen de bferro

Mas que no tener sentido, no hay un valor de default aceptable

Si Ceylon abandona null, entonces no existe un valor de default aceptable para los tipos referencias, lo que obliga entonces a inicializar valores de esos tipos.
Por otra parte, se puede argumentar que las variables de instancia casi siempre son inicializadas con algún valor pertinente en el constructor en lenguajes como Java, C++, etc. En Ceylon, el cuerpo de la clase es el propio constructor, por lo que entonces la inicialización forzada de las variables de instancia es natural.
Gavin dice que no hay constructor, lo que estrictamente es cierto si nos referimos a la sintaxis para el constructor de lenguajes conocidos. Yo prefiero decir que hay un constructor primario que es el propio cuerpo de la clase, considerando que siempre existe un código para inicializar el estado de un objeto, una vez que ha sido creado en memoria, código que llamamos constructor escríbase donde se escriba en la definición de la clase.

NullObjectPatternExiste

NullObjectPattern

Existe también el patrón de diseño "ObjetoNulo" ( NullObjectPattern pues ) que dice que se puede crear un objeto "vacío" que sea válido y se pueda usar como placeholder cuando no haya un valor seleccionado.

Sin embargo y aunque se ve muy atractivo, este patrón es poco usado, la razón es básicamente la misma que se está mencionando aquí, muchas veces no hay un valor predefinido útil y se puede causar más daño ocultando los problemas. El problema es que un atributo no tiene un valor válido pero en vez de tener un NullPointerException que lo indique, se tiene silencio. O mejor aún, como lo propone Ceylon y otros lenguajes antes de él, se le pone un valor válido.

Por cierto, esta es también una recomendación que viene en effectiva Java y para no darle tantas vueltas basta con decir que se debe marcar todos los atributos de una clase como final; verán que son forzados a crear el valor inicial en el constructor de la clase.

Interesante tema sin duda.

Aquí les dejo un link a StackOverflow sobre este patrón y si sería bueno o no asignarle el valor por omisión a todos los atributos.

http://stackoverflow.com/questions/3266179/is-it-feasible-to-create-a-nu...

Imagen de bferro

El patrón Null Object es natural en Ceylon

Lo que hace Ceylon en su tratamiento de null como un objeto de tipo Nothing es precisamente incorporar en el lenguaje el patrón Null Object.
Scala lo hace con Option siempre que sigas ese contrato.

Imagen de ezamudio

interés!=null

Lo que me da gusto es que el interés no es nulo.

Ya tengo que prepararme para mi primera plática sobre este tema, en el SG virtual el 22 de marzo:

http://www.sg.com.mx/sgvirtual/2012/program/sessions/proposed?order=coun...