Un PoC de REPL para Dart
Hackeando con Dart
Python tiene un modo interactivo genial. Dart no tiene un modo interactivo en este momento, pero es realmente bueno para prototipar ideas rápidamente, así que ¡veamos si podemos hackear algo juntos!
Aviso: Trabajo en Google, pero este post es sobre un proyecto personal. No estoy en el equipo de Dart ni nada relacionado. Esta es solo mi humilde opinión y mi historia. Acompáñame.
El modo interactivo en lenguajes como Python o Ruby ha ayudado a hacerlos muy accesibles para principiantes. Pero estos no son los únicos que usan el concepto de REPL. Un REPL es un ciclo read-eval-print. Shells como BASH o zsh también usan REPLs cuando interactúas con ellos en una terminal. También es lo que obtienes cuando trabajas con notebooks en Jupyter (IPython Notebooks), Matlab, Mathematica o Maple. Esta forma de computación interactiva es muy popular entre los investigadores.
Esto es lo que parece el modo interactivo de Python:
Dart no soporta evaluar sentencias en un scope global como Python, así que no hay una forma obvia de hacerlo correctamente. En Dart, las sentencias tienen que estar dentro de funciones y la función main es el punto de entrada de un programa. Igual que en C++ o C#. Personalmente, prefiero esto porque facilita averiguar qué está pasando cuando un programa se ejecuta. Pero… aún así me gustaría mucho tener un modo interactivo. Me permitiría jugar con ideas y probar cosas aún más rápido. Así que ¡veamos si podemos crear un REPL como prueba de concepto!
¿Cómo podemos crear un REPL en Dart?
#Dart es realmente bueno para prototipar. Así que hagamos justo eso y no nos preocupemos por preguntas de diseño del lenguaje :)
Ahora no hay función eval como en JavaScript o Python, y no quiero escribir un intérprete completo yo mismo para implementar una. No sería muy rápido y tampoco tengo mucho tiempo. Sin embargo, cuando depuras
código Dart en Intellij, puedes evaluar expresiones mientras recorres tu código. Evaluar expresiones es exactamente lo que queremos hacer, ¿no? ¿Podemos usar esta característica?
Las capacidades de depuración de Dart están expuestas a través de su VM service. Es un servicio JSON-RPC proporcionado por la
Dart VM al que puedes conectarte para depurar tu aplicación.
Natalie Weizenbaum ha publicado un artículo sobre el paquete
vm_service_client
que proporciona una API muy agradable para interactuar con el VM service: http://news.dartlang.org/2016/05/unboxing-packages-vmserviceclient.html
Ahora lo que podemos hacer es: ¡nuestro REPL puede conectarse a su propio VM service para evaluar expresiones que lee desde la terminal! Eso suena loco…
… ¡pero funciona! He escrito un spike rápido y efectivamente funciona. Dart soporta programación asincrónica, lo cual es muy útil ya que evita que el programa se bloquee a sí mismo al hablar con su propio VM service.
Los spikes son un concepto del desarrollo guiado por pruebas: son experimentos rápidos y sucios que uno escribe para resolver algunas preguntas técnicas.
Con la cuestión de factibilidad resuelta, es fácil escribir una prueba de concepto adecuada. Para evaluar expresiones que van más allá de
1 + 1, necesitamos soportar variables. Ahora las variables no se pueden crear fácilmente porque una declaración de variable no es una expresión en Dart. En Python, puedes declarar una variable al vuelo asignándole un valor. En Dart, podemos simular campos dinámicos sobrecargando
n[oSuchMethod](https://www.dartlang.org/articles/language/emulating-functions#interactions-with-mirrors-and-nosuchmethod)
para crear elementos en un diccionario al vuelo. Llamo a esta clase Scope y cuando evaluamos expresiones dentro de una instancia
de ella, los campos pueden ser accedidos como globales.
El código es en realidad más directo:
// Copyright (c) 2016, Andreas 'blackhc' Kirsch. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
import 'dart:mirrors';
Scope currentScope;
@proxy
class Scope {
final _scope = <Symbol, dynamic>{};
Scope();
Scope.predefined(Map<Symbol, dynamic> symbols) {
_scope.addAll(symbols);
}
factory Scope.clone(Scope other) => new Scope.predefined(other._scope);
@override
dynamic noSuchMethod(Invocation invocation) {
if (invocation.isGetter) {
if (_scope.containsKey(invocation.memberName)) {
return _scope[invocation.memberName];
} else {
return super.noSuchMethod(invocation);
}
} else if (invocation.isSetter) {
final variable = MirrorSystem.getSymbol(
MirrorSystem.getName(invocation.memberName).split('=').first);
_scope[variable] = invocation.positionalArguments.first;
return null;
} else if (invocation.isMethod) {
return Function.apply(_scope[invocation.memberName] as Function,
invocation.positionalArguments, invocation.namedArguments);
} else {
throw new UnsupportedError('Neither setter, nor getter, nor method!');
}
}
@override
String toString() => _scope.toString();
}
Con esto, ya podemos hacer cosas como a = 3y b = a*3:
Una limitación con la que nos encontramos rápidamente es que solo podemos acceder a símbolos que han sido importados en el archivo que declara la clase
Scope. import '…'; no puede ser evaluado usando el VM service. Así que no dart:io
si no lo importamos explícitamente, y no librerías personalizadas :(
¡Espera! Dart puede generar nuevos isolates (workers independientes) usando un URI con Isolate.spawnUri. Los usuarios podrían especificar imports adicionales en la línea de comandos, y el REPL podría generar código fuente para incluir estos imports y luego generar un nuevo isolate usando el código generado que tiene los imports disponibles para el usuario.
Y funciona \o/
Soportando más Dart
#Ahora otro problema es que solo podemos evaluar expresiones. Sentencias de control como bloques if/else
o bucles while no son expresiones. Para las sentencias, podríamos envolverlas en un closure y ejecutar el closure, que es una expresión de llamada a función. Así,
if (a == 1) print('a is 1!!'); se convertiría en
() { if (a == 1) print('a is 1!!'); }();
Solo necesitamos averiguar si la entrada es una expresión o una sentencia. Esto es difícil porque tendríamos que escribir un parser de Dart para esto. Pero Dart está escrito en Dart, y el paquete
analyzer proporciona un parser que puede parsear cualquier código Dart gratis.
// Copyright (c) 2016, Andreas 'blackhc' Kirsch. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
import 'dart:io';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/parser.dart';
bool _tryParse(String code, Function parse) {
final reader = new CharSequenceReader(code);
final errorListener = new BooleanErrorListener();
final scanner = new Scanner(null, reader, errorListener);
final token = scanner.tokenize();
final parser = new Parser(null, errorListener);
final node = parse(parser, token) as AstNode;
return !errorListener.errorReported &&
node != null &&
node.endToken.next.type == TokenType.EOF;
}
bool isExpression(String code) => _tryParse(
code, (Parser parser, Token token) => parser.parseExpression(token));
bool isStatements(String code) => _tryParse(code, (Parser parser, Token token) {
final statements = parser.parseStatements(token);
if (statements.isEmpty) {
return null;
}
return statements.last;
});
Y así, eso también está resuelto.
Más imports
#El último detalle que nos impide importar cualquier librería es que por defecto un nuevo Isolate solo ve los paquetes mencionados en su
pubspec.yaml. Queremos soportar importar cualquier librería sin embargo. Isolate.spawnUri
tiene un parámetro packageConfig que nos permite especificar un map de nombre de paquete a ruta de paquete. Podemos simplemente usar otro parámetro de línea de comandos para apuntar a otro paquete y usar su package config en nuestro Isolate. ¡Woohoo!
Rápidamente nos encontramos con el problema de que nuestro Isolate necesita acceder al paquete analyzer (y otros) que podrían no estar cargados por cualquier paquete con el que quieras jugar en tu sesión de REPL. ¡El paquete
package_resolver al rescate! Con él, podemos manipular fácilmente las configuraciones de paquetes.
import 'package:package_resolver/package_resolver.dart';
Future<PackageResolver> mergePackageConfigs(String otherPackageRoot) async {
final currentConfig = await PackageResolver.current.packageConfigMap;
final otherConfig =
(await SyncPackageResolver.loadConfig(otherPackageRoot + '/.packages'))
.packageConfigMap;
// We do something horrible here: We simply merge the current config with
// the other. Hoping it will still work..
final config = <String, Uri>{};
config.addAll(currentConfig);
config.addAll(otherConfig);
return new PackageResolver.config(config);
}
Con todo esto, tenemos un flujo de trabajo completo implementado:
¿Qué sigue?
#Este fue un post largo… Toda la prueba de concepto suma 451 líneas de código, casi tan largo como este post.
El código se puede encontrar en https://github.com/BlackHC/dart_repl. Puedes probarlo fácilmente si tienes Dart instalado:
pub global activate dart_repl
pub global run dart_repl
Realmente disfruté creando esta prueba de concepto en mi tiempo libre. Todas las piezas encajaron en un par de horas. El soporte IDE para Dart en IntelliJ es excelente, y hay muchísima documentación y artículos ahora. Revisa
la serie Unboxing Packages de Natalie Weizenbaum por ejemplo: http://news.dartlang.org/2016/04/unboxing-packages-async-part-3.html
entre otros. Hacking a bajo nivel en Dart es divertido, y hay grandes librerías para ser creativo. code_builder
se ve muy prometedor y built_collection
proporciona colecciones inmutables. David Morgan también ha estado publicando artículos sobre
colecciones inmutables
en Dart.
Para dart_repl, sería bueno importar librerías adicionales en runtime sin reinicios. El equipo de Dart ha añadido recientemente soporte para hot reloading al VM. Esto principalmente proporciona una mejor experiencia en
Flutter para desarrolladores de apps móviles. Tal vez, esto podría usarse para imports adhoc y para definir funciones y clases en el REPL también.
En general, Dart podría ser genial para investigación y para investigadores, y me encantaría ver soporte de Jupyter para Dart. Solo habría que implementar su interfaz de kernel para hacer un REPL de Dart compatible con ello… :) Eso debería ser fácil, ¿no?
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.