Dart obtiene un sistema de tipos

Alegraos, todos los fieles, porque Dart 2.0 viene en camino, y trae tipos.

Dos personas saltando en silueta al atardecer.

Este es el primer gran cambio que rompe la compatibilidad de Dart — de ahí el “2” — y es precisamente porque es un cambio que rompe la compatibilidad que es un desarrollo tan importante. Vamos a profundizar en por qué es así. Primero, algo de código de Dart 1:

void main() {
  cleanUp([new TempFile(), new BankAccount()]);
}

void cleanUp(List<TempFile> files) =>
    files.forEach((f) => f.delete());

class TempFile {
  void delete() => print('TempFile deleted.');
}

class BankAccount {
  void delete() => print('BankAccount deleted. Whoops!');
}

> TempFile deleted.
> BankAccount deleted. Whoops!

El código es un poco sorprendente.

Tanto “TempFile” como “BankAccount” tienen un método “delete”; resulta que como Dart 1 no se preocupa por los tipos por defecto, felizmente nos deja llamar “delete” en un “BankAccount” cuando estábamos claramente intentando llamar “delete” en un “TempFile”.

Nada bien.

Análisis de Strong Mode

#

Entremos en Strong Mode, el nuevo sistema de tipos para Dart. (No necesitaremos el nombre una vez que llegue Dart 2.0; será simplemente “el sistema de tipos”).

El dart analyzer ha soportado el análisis estático de Strong Mode desde hace tiempo; usa inferencia de tipos para deducir qué tipos probablemente querías decir, y requiere que cada variable solo pueda tener un único tipo. También añade rigor respecto a los overrides y los genéricos.

Si le damos el fragmento de código anterior al analyzer con Strong Mode habilitado, nos dice:

error: The element type 'BankAccount' can't be assigned to the list type 'TempFile'.

…lo cual parece razonable. Problema resuelto. ¿O no?

Lamentablemente no. Hay algunas formas de engañar al análisis estático. Por ejemplo, podemos usar “new List.from”, que toma cualquier “Iterable”:

void main() {
  var list = new List<TempFile>.from(
      [new TempFile(), new BankAccount()]);
  cleanUp(list);
}

> TempFile deleted.
> BankAccount deleted. Whoops!

…y el analyzer es impotente para ayudar. Simplemente no hay forma de detectar este problema con comprobaciones estáticas.

Strong Mode propiamente dicho

#

Aquí nos acercamos al concepto de soundness; si el sistema de tipos es capaz de cumplir lo que promete. Las comprobaciones estáticas adicionales vía el analyzer ayudan a detectar bugs, pero no son suficientes para cerrar todos los agujeros. Lo que se necesita son algunas comprobaciones adicionales en tiempo de ejecución.

Actualmente solo hay un runtime de Dart que implementa esas comprobaciones; el Dart Dev Compiler (DDC). Si ejecutamos ese último fragmento con DDC obtenemos un error en tiempo de ejecución:

Type 'BankAccount' is not a subtype of type 'TempFile'

Así que, nuestra cuenta bancaria está a salvo. Strong Mode con comprobaciones en tiempo de ejecución garantiza que si estamos intentando ejecutar “TempFile.delete” nunca, bajo ninguna circunstancia[1], ejecutaremos “BankAccount.delete” en su lugar.

Dart 2.0

#

Y aquí volvemos al anuncio sobre Dart 2.0, y por qué es importante.

La razón por la que DDC es el único runtime que añade estas comprobaciones es que son cambios que rompen la compatibilidad del lenguaje. Programas que solían ser válidos ya no lo son. DDC es una herramienta de desarrollo, así que no necesita compilar y ejecutar todo el Dart válido; la VM y dart2js sí.

Así que con el cambio a Dart 2.0, esos programas con problemas de tipos ahora pueden ser declarados inválidos — y todos los runtimes pueden avanzar hacia proporcionar tipos que cumplan lo que prometen. Un final feliz, y un paso importante para mi lenguaje de programación favorito.

Nota: Y nada de valor se perdió

#

Es natural preguntarse por los inconvenientes del cambio. ¿Qué pasa si queremos tipos flexibles de Dart?

Afortunadamente el lenguaje lo cubre. Siempre podemos excluirnos de las comprobaciones de tipos con el tipo especial “dynamic”. De hecho, “dynamic” es el valor por defecto si no añades una anotación de tipo, así que simplemente podríamos haber escrito “List” en lugar de “List<TempFile>”:

void main() {
  cleanUp([new TempFile(), new BankAccount()]);
}

void cleanUp(List files) =>
    files.forEach((f) => f.delete());

class TempFile {
  void delete() => print('TempFile deleted.');
}

class BankAccount {
  void delete() => print('BankAccount deleted. Whoops!');
}

> TempFile deleted.
> BankAccount deleted. Whoops!

…y tenemos código perfectamente válido de Dart 2.0[2]. El método “cleanUp” ahora está escrito para funcionar con cualquier clase que tenga un método “delete” — y eso es exactamente lo que hace.

[1] Como otros lenguajes han demostrado, lograr un sistema de tipos 100% sound es difícil. Pero no habrá ninguna forma conocida de romper el sistema de tipos.

[2] Dart 2.0 aún no existe, así que esto no está 100% garantizado. Pero en cuanto a los tipos, esto debería estar bien.

Más de Dart