Compartir a través de



Noviembre de 2018

Volumen 33, número 11

. NET: crear un lenguaje de script propio con delegados simbólicos

Por Thomas Hansen | Noviembre de 2018

Me encanta mi compilador de C#. Mi novia dice que lo quiero más que a ella, pero, por razones obvias, nunca lo admito en público. Tipado fuerte, genéricos, LINQ, recolección de elementos no utilizados, CLI, etc. La lista de cualidades atractivas es infinita. Sin embargo, de vez en cuando, necesito alejarme un par de días de la confortable seguridad que mi compilador suele proporcionarme. Durante esos días, elijo codificar con lenguajes de programación dinámicos y, cuanto más dinámico es el lenguaje, más peligroso y divertido me resulta.

Bien. Basta de introducciones. Ahora haremos lo que hacen los programadores reales cuando se quedan solos, crear código:

¡Hola mundo!

¿Qué? ¿Cree que dije "código"? Sí, eso dije, y ese texto es realmente código. Para que se entienda, vamos a ver lo que puede hacer con este código. Eche un vistazo a la Figura 1, que muestra un ejemplo minimalista de qué quiero decir con “delegados simbólicos”. Cree una aplicación de consola vacía en Visual Studio y escriba el código de la Figura 1.

Figura 1 Ejemplo minimalista de delegados simbólicos

using System;
using System.Collections.Generic;
class MainClass
{
  public static void Main(string[] args)
  {
    // The "programming language"
    var keywords = new Dictionary<string, Action> {
      // The "hello" keyword
      ["hello"] = () => {
        Console.WriteLine("hello was invoked");
      },
      // The "world" keyword
      ["world"] = () => {
        Console.Write("world was invoked");
      }
    };
    // The "code"
    string code = "hello world";
    // "Tokenising" the code
    var tokens = code.Split(' ');
    // Evaluating the tokens
    foreach (var token in tokens) {
      keywords[token]();
    }
  }
}

Con solo 25 líneas de código, he creado mi propio lenguaje de microprogramación. Para comprender sus ventajas, tenga en cuenta que la variable "code" puede enviarse a través de la red, se puede capturar de una base de datos y se puede almacenar en un archivo, y también puedo cambiar de forma dinámica la cadena de "Hola mundo" por "Mundo, hola" o "Hola hola mundo mundo" para ese fin y cambiar completamente el resultado para el que se evalúa. Esta capacidad significa que puedo combinar delegados de forma dinámica para obtener una lista de objetos de función, que se evalúa secuencialmente según el contenido del código. De repente, he transformado un lenguaje de programación compilado de forma estática en un lenguaje de scripting dinámico, simplemente dividiendo una frase en sus palabras y usando las palabras individuales como claves de búsqueda en un diccionario. El diccionario de la Figura 1, por lo tanto, se convierte en un "lenguaje de programación". De hecho, es un delegado simbólico.

En este ejemplo, obviamente, no es tan interesante y está pensado únicamente para ilustrar la idea principal. Voy a crear algo un poco más interesante. Para ello, me meteré en la madriguera del conejo para perseguir estas ideas, como se muestra en la Figura 2.

Figura 2 Encadenamiento dinámico de los delegados

using System;
using System.Linq;
using System.Collections.Generic;
public class Chain<T> : List<Func<T, T>>
{
  public T Evaluate(T input)
  {
    foreach (var ix in this)
    {
      input = ix(input);
    }
    return input;
  }
}
class MainClass
{
  public static void Main(string[] args)
  {
    var keywords = new Dictionary<string, Func<string, string>>
    {
      ["capitalize"] = (input) =>
      {
        return input.Replace("e", "EE");
      },
      ["replace"] = (input) =>
      {
        return input.Replace("o", "0");
      }
    };
    string code = "capitalize replace";
    var tokens = code.Split(' ');
    var chain = new Chain<string>();
    chain.AddRange(tokens.Select(ix => keywords[ix]));
    var result = chain.Evaluate("join the revolution, capitalize and replace");
    Console.WriteLine(result);
  }
}

Ahora tengo algo que parece útil: la capacidad de encadenar de forma dinámica los delegados, lo que resulta en un objeto lambda de funciones débilmente acopladas. Así, el código declara una cadena de funciones que transforma un objeto de manera secuencial, de acuerdo con los símbolos que decido incluir en mi código. Para el registro, normalmente no debería heredar la lista, pero para abreviar el ejemplo, decidí hacerlo para ilustrar la idea principal.

La ampliación basada en esta idea es simple. El ejercicio es encontrar el denominador común más pequeño para un delegado genérico que pueda describir cualquier construcción de programación que conozca, que resulta ser el siguiente delegado:

delegate object Function(List<object> arguments);

Este delegado puede representar casi cualquier estructura de programación jamás inventada. En el mundo de la informática, todo puede obtener una lista de argumentos de entrada y devolver algo de vuelta al autor de llamada. Este delegado es la misma definición de entrada/salida fundamental para todas las ideas informáticas y se convierte en una estructura de programación atómica que puede usar para solucionar todos sus problemas informáticos.

Conocer Lizzie

Al escribir este artículo, creé un lenguaje de programación que incorpora la idea anterior. Escribí el lenguaje completo, que llamé Lizzie, por mi novia, Lisbeth, en un par de fines de semana de luna llena completos. El lenguaje está incluido completamente en un único ensamblado, de aproximadamente 2000 líneas de código. Cuando se compila, solo tiene 45 KB en el disco y su objeto "compiler" solo tiene 300 líneas de código C#. Lizzie también se puede ampliar fácilmente y permite a cualquiera agregar sus propios objetos "keywords", lo que le permite crear fácilmente su propio lenguaje específico de dominio (DSL). Un caso de uso para este tipo de lenguaje es un motor basado en reglas, donde es necesario unir el código de forma más dinámica que la que permite C#. Con Lizzie, puede agregar código de script dinámico a su aplicación de C# compilada de forma estática, formado por fragmentos de código Turing completos de funcionalidad. Lizzie es para C# lo que las especias para la comida. No quiere comer solo especias, pero si sazona con especias un filete, la experiencia será más agradable. Para probar Lizzie, cree una aplicación de consola vacía en C#, agregue Lizzie como un paquete de NuGet y use el código de la Figura 3.

Figura 3 Creación de un lenguaje específico de dominio

using System;
using lizzie;
class Demo1
{
  [Bind(Name = "write")]
  object write(Binder<Demo1> binder, Arguments arguments)
  {
    Console.WriteLine(arguments.Get(0));
    return null;
  }
}
class MainClass
{
  public static void Main(string[] args)
  {
    var code = "write('Hello World')";
    var lambda = LambdaCompiler.Compile(new Demo1(), code);
    lambda();
  }
}

Con solo 22 líneas de código puedo decir que he creado mi propio DSL y agregado mi propia palabra clave específica de dominio al lenguaje.

La característica principal de Lizzie es que puede enlazar el código Lizzie a un tipo de contexto. El método LambdaCompiler.Compile de la Figura 3 es, de hecho, un método genérico, pero su argumento de tipo se deduce automáticamente de su primer argumento. Internamente, Lizzie creará un enlazador que se enlaza con su tipo, lo que provocará que todos los métodos con el atributo Bind estén disponibles en el código Lizzie. Cuando el código Lizzie se evalúa, tiene una palabra clave adicional denominada "write". Puede enlazar cualquier método al código Lizzie, siempre y cuando tenga la firma correcta. También puede enlazar el código Lizzie a cualquier tipo.

Lizzie tiene varias palabras clave predeterminadas, que pone a su disposición para su propio código, pero no tiene que usarlas si no quiere. La Figura 4 muestra un ejemplo más completo que usa algunas de estas palabras clave.

Figura 4 Uso de algunas palabras clave predeterminadas

using System;
using lizzie;
class Demo1
{
  [Bind(Name = "write")]
  object write(Binder<Demo1> binder, Arguments arguments)
  {
    Console.WriteLine(arguments.Get(0));
    return null;
  }
}
class MainClass
{
  public static void Main(string[] args)
  {
    var code = @"
// Creating a function
var(@my-function, function({
  +('The answer is ', +(input1, input2))
}, @input1, @input2))
// Evaluating the function
var(@result, my-function(21,2))
// Writing out the result on the console
write(result)
";
    var lambda = LambdaCompiler.Compile(new Demo1(), code);
    lambda();
  }
}

El código Lizzie de la Figura 4 crea primero una función denominada "my-function" y, luego, la invoca con dos argumentos de entero. Finalmente, escribe el resultado de la invocación de la función en la consola. Con 21 líneas de código C# y ocho líneas de código Lizzie, he evaluado un fragmento de código dinámico que crea una función en un lenguaje de scripting dinámico, desde mi código C#, a la vez que he agregado una nueva palabra clave para el lenguaje de scripting que estoy usando. Se necesitaron solo 33 líneas de código en total, incluidos los comentarios. Estas 33 líneas de código le permiten notificar que ha creado su propio lenguaje de programación. Anders Hejlsberg, move over rover, and let little Jimmy take over…

¿Es Lizzie un lenguaje de programación "real"?

Para responder a esa pregunta, debe pensar en lo que considera un lenguaje de programación real. Lizzie es Turing completo y, al menos en teoría, le permite solucionar todos los problemas informáticos que pueda imaginar. Por lo tanto, según la definición formal de lo que constituye un lenguaje de programación "real", es ciertamente tan real como cualquier otro lenguaje de programación. Por otro lado, ni se interpreta ni se compila, porque cada invocación de función es, simplemente, una búsqueda en el diccionario. Además, incluye solo un puñado de construcciones y todo se centra en la sintaxis "function(arguments)". De hecho, aunque las instrucciones siguen la sintaxis de la función definida previamente en el delegado genérico:

// Creates a function taking one argument
var(@foo, function({
  // Checking the value of the argument
  if(eq(input, 'Thomas'), {
    write('Welcome home boss!')
  }, {
    write('Welcome stranger!')
  }) // End of if
}, @input)) // End of function
// Invoking the function
foo('John Doe')
The syntax of if is as follows:
if(condition, {lambda-true}, [optional] {lambda-else})

El primer argumento para la palabra clave "if" es una condición. El segundo argumento es un bloque lambda que se evalúa si la condición da como resultado un valor no nulo (true). El tercer argumento es un bloque lambda opcional que se evalúa si la condición da como resultado un valor nulo (false). Así, si la palabra clave “if” es una función a la que puede suministrar un argumento lambda, use la sintaxis “{ … code … }” para declarar el valor la expresión lambda. Esto puede parecer un poco extraño al principio, porque todo sucede entre los paréntesis de apertura y cierre de las palabras clave, a diferencia de otros lenguajes de programación que usan una sintaxis más tradicional. Sin embargo, para crear un compilador de lenguaje de programación en 300 líneas de código, era necesario tomar algunas decisiones audaces. Y Lizzie es todo simplicidad.

Las funciones de Lizzie son sorprendentemente similares a la estructura de una expresión S de Lisp, aunque sin la extraña notación polaca. Dado que una expresión S puede describir cualquier elemento y las funciones de Lizzie son, simplemente, expresiones S con el símbolo (primer argumento) fuera de los paréntesis, puede describir lo que quiera con Lizzie. Con esto, se puede decir que Lizzie se convierte en una implementación dinámica de Lisp para Common Language Runtime (CLR), con una sintaxis más intuitiva para los desarrolladores de C#/JavaScript. Permite agregar código dinámico sobre el código C# compilado de forma estática, sin tener que leer miles de páginas de documentación para aprender un nuevo lenguaje de programación. De hecho, toda la documentación de Lizzie consta solo de 12 páginas de texto, lo que significa que un desarrollador de software puede aprender el lenguaje Lizzie literalmente en unos 20 minutos.

Lizzie: JSON de código

Una de las características que más me gusta de Lizzie es su falta de características. Permítame ilustrarlo con una lista parcial de lo que Lizzie no puede hacer. Lizzie no puede:

  • leer ni escribir desde el sistema de archivos;
  • ejecutar consultas SQL;
  • pedirle su contraseña;
  • cambiar el estado de su equipo en absoluto.

De hecho, un fragmento de código Lizzie predefinido no puede ser malintencionado, ni siquiera en teoría. Esta falta de características proporciona a Lizzie algunas capacidades exclusivas que el scripting de Roslyn y C# no ofrece.

En su estado original, Lizzie es completamente seguro, lo que le permite transmitir código de forma segura a través de una red, de un equipo a otro, de la misma manera que usaría JSON para transmitir datos. A continuación, en el punto de conexión que acepta el código Lizzie, tendría que implementar explícitamente la compatibilidad con las funciones a las que el código Lizzie necesite acceder con la finalidad de que funcione para su caso de uso. Podría incluir un método de C# que lea los datos de una base de datos SQL o la capacidad para actualizar datos en una base de datos SQL, o para leer o escribir archivos. No obstante, todas estas invocaciones de funciones se pueden retrasar hasta que haya comprobado que el código tiene permiso para hacer lo que está intentando hacer. Por tanto, puede implementar fácilmente la autenticación y la autorización antes de permitir que ejecute "insert sql", "read file" o cualquier otro comando.

Esta propiedad de Lizzie le permite crear un punto de conexión HTTP de REST genérico donde la capa de cliente envíe el código Lizzie para su servidor y se evalúe posteriormente. A continuación, puede hacer que su servidor cree una respuesta JSON que devuelva al cliente. Y lo más interesante, se puede implementar de forma segura. Puede implementar un único punto de conexión HTTP de REST que acepte solo las solicitudes POST que contengan código Lizzie y reemplace literalmente el back-end completo por un evaluador de Lizzie totalmente dinámico y genérico. Esto le permite mover toda la lógica de negocios y la capa de acceso a datos a su código de front-end, de manera que el código de front-end cree dinámicamente código Lizzie que transmita al servidor para su evaluación. Además, puede hacerlo de forma segura, suponiendo que autentique y autorice a los clientes antes de permitirles evaluar el código Lizzie.

Básicamente, toda su aplicación, de repente, se compila fácilmente en JavaScript, TypeScript, ObjectiveC o cualquier otro lenguaje. Puede compilar clientes en cualquier lenguaje de programación que le guste y que cree código Lizzie de forma dinámica, y enviar este código al servidor.

Lecciones de Einstein

Cuando Albert Einstein escribió su famosa ecuación para explicar el universo, solo tenía tres componentes simples: energía, masa y la velocidad de la luz al cuadrado. Cualquier adolescente de 14 años con unos conocimientos adecuados de matemáticas podía entender fácilmente la ecuación. Comprender la programación de equipos, por otro lado, requiere miles de páginas y millones (si no billones) de palabras, acrónimos, tokens y símbolos, además de toda una sección de Wikipedia sobre paradigmas, numerosos patrones de diseño y una gran variedad de lenguajes, cada uno con estructuras e ideas completamente diferentes, que requieren marcos y bibliotecas "fundamentales" que necesita agregar a su "solución" para poder empezar a trabajar en el problema del dominio. Se espera que sepa todas estas tecnologías de memoria para poder presentarse como desarrollador de software intermedio. ¿Soy el único que percibe este problema?

Lizzie no es una panacea, como tampoco lo son los delegados simbólicos, pero son definitivamente un paso en la dirección de "20 GOTO 10". En ocasiones, para avanzar, debe retroceder un paso. En ocasiones deberá observarse a sí mismo de manera objetiva desde fuera. Como comunidad profesional, si lo hacemos, podríamos darnos cuenta de que la cura para nuestra locura es la simplicidad, y no 50 patrones de diseño más, 15 nuevos lenguajes de consulta, 100 nuevas características de lenguaje y un millón de bibliotecas y marcos nuevos, cada uno con un billón de componentes móviles.

Menos es más siempre, ¡así que denme más menos y menos más! Si quiere menos, únase a mí en github.com/polterguy/lizzie. Aquí es donde encontrará Lizzie, sin seguridad de tipos, sin palabras clave, sin operadores, sin OOP y, claramente, no como una biblioteca o un marco visible.

Resumen

La mayoría de la gente del sector informático tiende a discrepar con mis ideas. Si preguntara al arquitecto de software medio qué opina de estas ideas, probablemente le lanzaría bibliotecas enteras encima como fuentes para probar lo equivocado que estoy. Por ejemplo, la suposición predominante en el desarrollo de software es que el tipado fuerte es adecuado y el tipado débil no lo es. Pero, para mí, la simplicidad es la única alternativa posible, incluso cuando se tiene que descartar el tipado fuerte. Tenga en cuenta, sin embargo, que las ideas como Lizzie están destinadas a "enriquecer" el código de C# con tipado estático existente, no a reemplazarlo. Aunque nunca utilice las ideas de codificación presentadas en este artículo directamente, comprender los conceptos clave puede ayudarle a escribir código estándar en un lenguaje de programación tradicional más eficazmente y ayudarle a trabajar para lograr el objetivo de simplicidad.

Lección sobre la historia de la programación

Cuando era un desarrollador júnior, solía crear aplicaciones de 3 niveles. La idea era separar las capas de datos de las capas de lógica de negocios y las capas de interfaz de usuario. El problema es que, a medida que el marco de front-end aumenta en complejidad, se ve obligado a crear una arquitectura de aplicaciones de 6 niveles. En primer lugar, debe crear una arquitectura del lado servidor de 3 niveles y, después, una arquitectura del lado cliente de 3 niveles. Y, como si no fuera suficiente, luego tiene que portar el código a Java, ObjectiveC o cualquier otro lenguaje que admita todos los clientes posibles que existen. Siento ser directo, pero este tipo de arquitectura es lo que yo llamo "diseño basado en la insensatez", porque aumenta la complejidad de las aplicaciones hasta el punto en que suelen ser prácticamente imposibles de mantener. A menudo, un solo cambio en la interfaz de usuario de front-end se propaga a través de 15 capas de arquitectura y cuatro lenguajes de programación diferentes, y le obliga a realizar cambios en todas las capas solo para agregar una columna simple en una cuadrícula de datos en el front-end. Lizzie soluciona este problema al permitir que el front-end envíe código al back-end, que el back-end evalúa y se devuelve al cliente como JSON. Cierto, pierde la seguridad de tipos, pero cuando la seguridad de tipos obliga a unir las distintas capas de las aplicaciones, de modo que los cambios de una ubicación se propaguen a todas las otras capas del proyecto, no vale la pena pagar el costo de la seguridad de tipos.

Empecé a programar cuando tenía 8 años, en un Oric 1 en 1982. Recuerdo claramente el primer programa informático que escribí. Hice lo siguiente:

10 PRINT "THOMAS IS COOL"
20 GOTO 10

Si un niño de 8 años hoy quiere reproducir esa experiencia, con todos los procedimientos recomendados, con un marco de cliente como Angular y un marco de back-end como. NET, probablemente tendrá que saber miles de páginas de literatura informática técnica de memoria. En cambio, comencé con un libro que apenas tenía 300 páginas y un puñado de revistas de informática. Antes de llegar a la página 100, había creado mi propio juego informático, con solo 8 años. Lo siento si esto me hace parecer viejo, pero no se trata de evolución y mejora, sino de "degeneración" y locura. Y sí, antes de que empiece a escribir sus objeciones de forma frenética, profesores y doctores mucho más inteligentes que yo han investigado científicamente este problema, y lo han observado y confirmado de manera neutral.

El hecho es que la programación informática como profesión (humana) está a punto de extinguirse, dado que la complejidad que se agrega continuamente puede llegar pronto a superar la capacidad del cerebro humano para crear programas informáticos. Dado que la programación se está volviendo tan exigente desde una perspectiva cognitiva, puede ser que ningún humano sea capaz de hacerlo dentro de 10 años. Al mismo tiempo, los humanos son cada día más dependientes de los equipos y del software. Podéis llamarme antiguo, pero me gusta pensar que un ser humano en algún lugar es capaz de entender cosas que son fundamentales para mi felicidad, existencia y calidad de vida.


Thomas Hansen crea software desde que tenía 8 años, cuando empezó a escribir código con un equipo Oric-1 en 1982. Se refiere a sí mismo como programador informático zen, que pretende reducir la complejidad de la programación moderna y se niega a divulgar ninguna creencia en dogmas tecnológicos. Hansen trabaja en Bright Code en Chipre, donde crea software tecnológico financiero.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey


Comente este artículo en el foro de MSDN Magazine