Evolucionando el PoC de REPL de Dart
Hackeando con Dart

El REPL de Dart te permite evaluar expresiones y declaraciones de Dart en una shell interactiva. Ha pasado un tiempo desde mi primer post sobre el REPL de Dart (no necesitas leerlo para disfrutar este post), y aún faltan muchas características. En particular, los imports dinámicos y el soporte para declaraciones de nivel superior serían muy útiles, así que veamos cómo soportarlos.
Aviso: Sí trabajo para Google, pero este post es sobre un proyecto personal. No estoy en el equipo de Dart ni relacionado. Este artículo solo contiene mi humilde opinión personal.
tl;dr: Código e instrucciones sobre cómo ejecutar el REPL de Dart se pueden encontrar en https://github.com/BlackHC/dart_repl.
Hot reload
#Para Flutter, se ha añadido una característica nueva e interesante al Dart VM: hot reload. Hay un clip entretenido de YouTube del Dart DevSummit que lo explica y muestra en detalle:
Hot reload te permite cambiar tu código mientras tu programa está en ejecución. El Dart VM recogerá los cambios que has hecho e intentará aplicarlos mientras mantiene todo funcionando. Y si no puede, te dirá por qué. ¡Esto es muy genial! En el espíritu de hackear Dart para hacer grandes cosas, pensemos cómo podemos usar esto para implementar las nuevas características.
¿Por qué no podemos importar nuevas bibliotecas en la versión actual del REPL?
#El REPL usa el VM service de Dart para evaluar expresiones. Lamentablemente, importar una biblioteca no es una expresión en Dart, así que no podemos simplemente evaluarla en ese contexto. Sin embargo, podemos cambiar el código del sandbox del REPL mientras está en ejecución para importar una nueva biblioteca y luego podríamos simplemente desencadenar un hot reload para actualizar el REPL. ¿Funciona? De hecho funciona \o/
Pero espera: otra cosa que no podemos hacer evaluando expresiones vía el VM service es crear nuevas clases y funciones. De hecho, ninguna de las declaraciones de nivel superior de Dart puede ejecutarse evaluando una expresión por esta razón.
¿Cómo podemos permitir declaraciones de nivel superior?
#Por supuesto, podríamos usar la misma idea descrita anteriormente para también añadir una nueva clase o una función global. Sin embargo, cualquiera que haya usado IPython o similar por un tiempo sabe que tiendes a redeclarar la misma clase o función frecuentemente mientras iteras en el código. Re-ejecutas versiones ligeramente modificadas del mismo código una y otra vez mientras juegas con él.
Si tuviéramos que simplemente añadir estas declaraciones a nuestra biblioteca sandbox de Dart, nos requerirría llevar un registro de qué se ha declarado cuándo y dónde en el archivo para actualizar la declaración cuando iteres en ella. Esto requiere mucha lógica y código inteligente. Lamentablemente, también se rompería fácilmente si un cambio a una clase rompiera la compatibilidad con código más antiguo u otras declaraciones. Esto impedirría que el REPL hiciera hot reload y forzaría al usuario a reiniciarlo :( Esto suena complejo y frágil: ¡no una combinación ganadora, creo!
Spike & chains
#En cambio, ¿qué tal si pudiéramos redefinir la misma declaración de nivel superior múltiples veces sin que las redeclaraciones colisionen jamás? ¿Es eso siquiera posible en Dart? ¡Por supuesto que lo es! Pero no dentro de la misma biblioteca :) Dart te permite importar una biblioteca y luego declarar una clase, función o variable global que oculte una declaración existente.
class MyClass {
static final value = 1;
}
import ‘a.dart’;
class MyClass {
static final value = 10;
}
En este ejemplo, no habrá quejas sobre que el MyClass de b.dart oculte la versión de
a.dart porque viven en bibliotecas diferentes y la declaración local en b.dart
tiene precedencia sobre la importada desde a.dart.
En general, shadowing es lo que ocurre cuando declaras una variable que oculta otra variable de un scope externo. Por ejemplo:
float a;
void func() {
float a; // This `a` shadows global `a`.
}
¿Podemos usar esto? Para investigarlo, he implementado un spike rápido aquí. No genera ningún código. Más bien, es un ejemplo muy simple para asegurar que lo que pensamos que funcionará realmente funcione. ¡Sería frustrante pasar mucho tiempo implementando esto usando generación de código solo para descubrir que nunca podría haber funcionado! La esencia es esta:
class A {
int f() => 1;
}
import 'import_1.dart';
export 'import_1.dart';
class A {
int f() => 2;
}
import 'import_2.dart';
void main() {
result = new A().f();
print(result);
}
¡Esto funciona de verdad! Podemos crear una cadena de bibliotecas que se importan entre sí (y también se exportan entre sí porque de lo contrario los símbolos no estarán disponibles en todas partes). Luego los usuarios pueden redefinir símbolos tantas veces como quieran. Obviamente, esto puede resultar en código antiguo refiriéndose a símbolos ocultos, lo cual podría hacer las cosas un poco confusas, pero al menos no se romperá. Y cualquiera que haya usado IPython o similar ha aprendido a vivir con eso también. No puede ser tan malo.
El diagrama anterior muestra cómo funciona: a medida que añadimos nuevas declaraciones de nivel superior, se crean nuevas “celdas” (bibliotecas de Dart) que importan (y exportan) la celda anterior. La celda final se importa en la biblioteca sandbox que se usa como entorno de ejecución para expresiones y declaraciones normales de Dart. El archivo sandbox se edita in-place y luego se recarga usando hot reload.
Una visión del flujo de trabajo
#Además, si quieres actualizar código continuamente sin ocultar nada, eso también es posible: hot reload ya permite este flujo de trabajo en programas normales de Dart. Puedes hacer lo mismo en el REPL. Puedes editar tu increíble biblioteca de Dart
amazing_dart_library.dart e importarla al REPL, jugar con ella, y mientras lo haces, puedes editar el código en tu editor de preferencia y hacer que el REPL haga hot reload del código cuando quieras llamando
reload(). Lo mejor de ambos mundos \o/
¿Cómo podemos implementar esto en la práctica?
#
Bueno, estamos hackeando Dart aquí, así que veamos: Hot reload no está soportado por el vm_service_client
todavía ya que es una característica tan nueva y la especificación del servicio
no está totalmente completa todavía. Empecé a escribir un pull request
para Natalie (la mantenedora) para añadir soporte para ello, pero realmente, como mis compañeros de trabajo saben: código de calidad de producción no es lo mío, especialmente no en mi tiempo libre (¡lo siento Natalie!). Sin embargo, esto no bloquea nuestra aventura de hacking.
pub, el sistema de gestión de paquetes de Dart, no solo soporta resolución automática de restricciones de versión y un repositorio centralizado de paquetes, también te permite
usar paquetes locales o depender directamente de GitHub
. Esto normalmente no es recomendable porque pierdes gran parte de lo que hace grandioso a pub, pero aquí funciona: simplemente hice fork de
vm_service_client en mi propio clon de GitHub e hice los cambios necesarios. Puedes encontrar el código en
https://github.com/BlackHC/vm_service_client/tree/reload_sources_poc. Después, cambié el
pubspec.yaml del REPL de Dart para enlazar a mi clon de GitHub en lugar de la versión oficial:
[...]
vm_service_client:
git:
url: git://github.com/BlackHC/vm_service_client.git
ref: reload_sources_poc
[...]
¡Y eso es todo! Un simplepub geten la terminal ahora actualiza el REPL de Dart para usar la versión del fork.
Esto hace que sea muy fácil experimentar con cualquier cosa: puedes hacer fork de otros paquetes para probar cosas y depender fácilmente de ellos. Y lo genial es que puedo publicar esto y cuando descargues el REPL para ti mismo usando pub, también tomará el código de GitHub. ¡Muy hackeable y a la vez compartible! (Incluso si no es recomendable para paquetes de producción en general :)
La parte principal de lógica jugosa es el generador de celdas que implementa un mecanismo de plantillas muy simple en línea con lo que hemos discutido anteriormente:
import 'dart:io';
class DartTemplate {
final String content;
DartTemplate(this.content);
void instantiate(String targetPath,
{String source, String library, String imports}) {
final instanceSource = content
.replaceAll('/*{SOURCE}*/', source ?? '')
.replaceAll('/*{IMPORTS}*/', imports ?? '')
.replaceAll('/*{LIBRARY}*/', library ?? '');
new File(targetPath).writeAsStringSync(instanceSource);
//print('wrote $targetPath:\n$instanceSource');
}
}
/// Keeps a chain of temporary cell files that import and re-export each other.
/// This allows us to create top-level cells that contains classes and other
/// top-level decls that can shadow each other.
class TopLevelCellChain {
final DartTemplate cellTemplate;
final String headName;
final String basePath;
int _currentCellIndex = 0;
TopLevelCellChain(this.cellTemplate, this.headName, this.basePath);
String get currentCellPath => '$basePath/$currentCellName';
String get currentCellName => 'cell${_currentCellIndex}.dart';
String get headPath => '$basePath/$headName';
void addCell(String source) {
// Import and export the previous cell.
final imports = _currentCellIndex > 0
? '''
// Import the previous cell and export it to make its symbols available to the
// next cell.
import '$currentCellName';
export '$currentCellName';
'''
: '';
_currentCellIndex++;
cellTemplate.instantiate(currentCellPath, imports: imports, source: source);
// Update the sandbox.
refreshSandboxLibrary();
}
void refreshSandboxLibrary() {
var libraryStatement = '''
/// This library name is needed to find the library using reflection.
library sandbox;
''';
cellTemplate.instantiate(headPath,
imports: _currentCellIndex > 0 ? 'import \'$currentCellName\';' : '',
library: libraryStatement);
}
void undoCell() {
if (_currentCellIndex > 0) {
_currentCellIndex--;
}
refreshSandboxLibrary();
}
}
La característica de hot reload se llama desde el REPL cuando se necesita un nuevo import:
Future linkAndExecuteCell(SandboxIsolate sandboxIsolate, String input,
VMRunnableIsolate runnableIsolate) async {
sandboxIsolate.cellChain.addCell(input);
final report = await runnableIsolate.reloadSources();
if (!report.status) {
print(report.message);
// Undo the last cell, so we can try again.
sandboxIsolate.cellChain.undoCell();
}
}
¡Y eso es más o menos todo! Puedes echar un vistazo a todos los cambios en el pull request: https://github.com/BlackHC/dart_repl/pull/2. Admito que el código es un poco hacky y desordenado. También hay bastante código wrapper no relacionado para el paso de mensajes entre el REPL y el sandbox en el pull request. Esto lamentablemente oscurece un poco los cambios principales. Necesito ver cómo podemos refactorizar todo esto para hacerlo más limpio y ordenado de nuevo… pero a veces, es más fácil hacer que las cosas funcionen rápidamente que escribir el mejor código y pull requests. ¡Lo siento por eso!
El código fuente del REPL de Dart se puede encontrar en https://github.com/BlackHC/dart_repl. Además de soportar declaraciones de nivel superior, también he añadido soporte para los comandos integrados import,
loadPackage y reload. (Nota: loadPackage requiere las builds dev 1.24 del Dart SDK que se publicarán pronto. De lo contrario es un no-op). Estos comandos integrados son todos extensiones triviales que usan hot reload. Finalmente, para cargar nuevos paquetes desde tu caché local de pub, estoy usando el excelente paquete
pub_cache.
Para probarlo (y asumiendo que tienes instalado el Dart SDK), solo ejecuta:
pub global activate dart_repl
pub global run dart_repl
¡Gracias por llegar al final de este artículo! Por favor dime qué piensas :)
Saludos, Andreas
Más de Dart
Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.