Novembro de 2018

Volume 33 – Número 11

.NET - Crie sua própria linguagem de Script com delegados simbólicos

Por Thomas Hansen | Novembro de 2018

Eu amo meu compilador C#. Minha namorada diz que gosto mais do compilador do que dela, ainda que, por motivos óbvios, eu nunca admitiria isso em público. Tipagem forte, genéricos, LINQ, coleta de lixo, CLI... a lista de características interessantes continua. No entanto, de vez em quando eu preciso passar alguns dias longe da zona de conforto gerada pelo meu compilador. Nesses dias, prefiro codificar em linguagens de programação dinâmicas e quanto mais dinâmica a linguagem, mais perigoso e divertido fica.

OK. Chega de introdução. Agora vamos fazer o que programadores de verdade fazem quando estão sozinhos, vamos criar códigos:

olá mundo

Como? Você achou que eu tivesse dito “código”? É, foi isso mesmo que eu disse. E esse texto, na verdade, é um código. Para entender melhor, vamos ver o que podemos fazer com ele. Dê uma olhada na Figura 1, que mostra um exemplo minimalista do que eu chamo de delegados simbólicos. Crie um aplicativo de console vazio no Visual Studio e digite o código da Figura 1.

Figura 1 Um exemplo 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]();
    }
  }
}

Em apenas 25 linhas de código, eu criei minha própria microlinguagem de programação. Para entender as vantagens, lembre-se de que a variável de “código” pode ser enviada pela rede, buscada em um banco de dados e armazenada em um arquivo. Além disso, é possível alterar dinamicamente a cadeia de caracteres de “olá mundo” para “mundo olá” ou até mesmo “olá olá mundo mundo”, alterando completamente o resultado que será avaliado. Com essa capacidade, eu posso combinar delegados dinamicamente para obter uma lista de objetos de função, que é avaliada sequencialmente, de acordo com o conteúdo do código. De repente, eu transformei uma linguagem de programação compilada estaticamente em uma linguagem de script dinâmica ao simplesmente dividir uma frase entre suas palavras e depois usar palavras individuais como chaves de pesquisa em um dicionário. O dicionário na Figura 1, portanto, torna-se uma “linguagem de programação”. Na verdade, trata-se de um delegado simbólico.

Claro que esse exemplo não é tão interessante e eu o usei apenas para ilustrar a ideia central. Vou criar algo um pouco mais interessante e aprofundando mais nessas ideias, conforme mostrado na Figura 2.

Figura 2 Encadeando delegados dinamicamente

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);
  }
}

Agora temos algo que parece ser quase útil: a capacidade de encadear delegados juntos dinamicamente, o que resulta em um objeto lambda de funções acopladas livremente. Dessa forma, o código está declarando uma cadeia de funções que transforma um objeto sequencialmente, de acordo com os símbolos escolhidos para entrarem no código. Para constar, não é comum você herdar da lista, mas para não alongar esse exemplo, decidi fazer isso para ilustrar a ideia principal.

Expandir a partir dessa ideia é simples. O exercício é encontrar o menor denominador comum para um delegado genérico e que possa descrever qualquer constructo de programação que você conheça, o qual, por acaso, é o delegado a seguir:

delegate object Function(List<object> arguments);

Esse delegado pode representar praticamente todas as estruturas de programação já inventadas. Tudo na computação pode conter uma lista de argumentos de entrada e retornar algo para o chamador. Esse delegado é a própria definição de entrada/saída fundamentais para todas as ideias de computação, tornando-se uma estrutura de programação atômica que você pode usar para resolver todos os seus problemas de computação.

Conheça Lizzie

Ao escrever este artigo, criei uma linguagem de programação abrangendo a ideia anterior. Eu escrevi a linguagem inteira em alguns fins de semana e a chamo de Lizzie, em homenagem à minha namorada, Lisbeth. A linguagem está toda em um único assembly, com aproximadamente 2.000 linhas de código. Compilada, ela ocupa apenas 45 KB do meu disco, e seu “compilador” tem apenas 300 linhas de código C#. A Lizzie também pode ser estendida facilmente e qualquer pessoa pode adicionar suas próprias “palavras-chave” a ela, permitindo que você crie facilmente sua própria linguagem específica de domínio (DSL). Um caso de uso para essa linguagem é um mecanismo baseado em regras, no qual você precisa amarrar o código de um modo mais dinâmico do que o C# permite. Com a Lizzie, você pode adicionar um código de script dinâmico ao seu aplicativo C# compilado estaticamente com snippets de funcionalidade computacionalmente universais. A Lizzie está para o C# como o tempero está para sua comida. Você não vai comer apenas o tempero, mas se colocar um pouco dele na sua refeição, obviamente sua experiência fica mais agradável. Para testar a Lizzie, crie um aplicativo de console vazio no C#, adicione a Lizzie como um pacote do NuGet e use o código da Figura 3.

Figura 3 Criando uma linguagem específica de domínio

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();
  }
}

Em apenas 22 linhas de código, criei meu próprio DSL e adicionei minha própria palavra-chave específica de domínio à linguagem.

O principal recurso da Lizzie é poder associar seu código Lizzie a um tipo de contexto. O método LambdaCompiler.Compile da Figura 3 é, na verdade, um método genérico, mas seu argumento de tipo é inferido automaticamente pelo seu primeiro argumento. Internamente, a Lizzie criará um associador que será associado ao seu tipo, disponibilizando todos os métodos com o atributo Bind no seu código Lizzie. Quando esse código for avaliado, apresentará uma palavra-chave extra chamada de “gravação”. Você pode associar qualquer método ao seu código Lizzie, desde que ele tenha a assinatura correta. Além disso, você também pode associar seu código Lizzie a qualquer tipo.

A Lizzie tem várias palavras-chave padrão, as quais são disponibilizadas para você usar no seu próprio código, mas não é preciso usá-las caso não queira. A Figura 4 mostra um exemplo mais completo que usa algumas dessas palavra-chave.

Figura 4 Usando algumas palavras-chave padrão

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();
  }
}

Primeiro, o código Lizzie da Figura 4 cria uma função chamada “my-function”, depois invoca essa função com dois argumentos inteiros. Por fim, ele grava o resultado da invocação da função no console. Com 21 linhas de código C# e oito linhas de código Lizzie, eu avaliei um trecho de código dinâmico que cria uma função em uma linguagem de script dinâmica a partir do meu código C# e adicionei uma nova palavra-chave à linguagem de script que estou usando. Foram apenas 33 linhas de código no total, incluindo comentários, que permitem a você afirmar que criou sua própria linguagem de programação. Anders Hejlsberg, pode deixar que nós assumimos daqui.

A Lizzie é uma linguagem de programação “real”?

Para responder a essa pergunta, preciso saber o que você considera ser uma linguagem de programação real. A Lizzie é computacionalmente universal e, pelo menos na teoria, permite que sejam resolvidos todos os problemas computacionais imagináveis. Então, de acordo com a definição formal do que constitui uma linguagem de programação “real”, certamente ela é tão real quanto qualquer outra linguagem de programação. Por outro lado, ela não é interpretada nem compilada, pois cada invocação de função se trata, simplesmente, de uma pesquisa de dicionário. Além disso, ela apresenta apenas alguns dos constructos, e tudo gira em torno da sintaxe “function(arguments)”. Na verdade, mesmo que as instruções sigam a sintaxe da função definida anteriormente no 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})

O primeiro argumento para a palavra-chave “if” é uma condição. O segundo argumento é um bloco lambda que é avaliado se a condição produzir um não nulo (true). O terceiro argumento é um bloco de lambda opcional que é avaliado se a condição produzir um nulo (false). Portanto, a palavra-chave “if” é, na verdade, uma função à qual você pode fornecer um argumento lambda usando a sintaxe “{ … código … }” para declarar o lambda. Isso pode parecer um pouco estranho no início porque tudo acontece entre os parênteses das suas palavras-chave, ao contrário de outras linguagens de programação, que usam uma sintaxe mais tradicional. No entanto, para criar um compilador de linguagem de programação em 300 linhas de código, algumas decisões duras tiveram de ser tomadas. E a Lizzie representa sobretudo simplicidade.

As funções da Lizzie são surpreendentemente semelhantes à estrutura de uma expressão s da Lisp, embora sem a estranha notação polonesa. Como uma expressão s pode descrever qualquer coisa e como as funções da Lizzie são simplesmente expressões s com o símbolo (primeiro argumento) fora dos parênteses, você pode descrever qualquer coisa com a Lizzie. Indiscutivelmente, isso transforma a Lizzie em uma implementação dinâmica da Lisp para o CLR (Common Language Runtime), com uma sintaxe mais intuitiva para desenvolvedores de C#/JavaScript. Ela permite que você adicione um código dinâmico sobre seu C# compilado estaticamente sem precisar ler milhares de páginas de documentação para aprender uma nova linguagem de programação. Na verdade, toda a documentação da Lizzie contém apenas 12 páginas de texto, o que significa que um desenvolvedor de software pode, literalmente, aprender a Lizzie em cerca de 20 minutos.

Lizzie — JSON para codificar

Um dos meus recursos favoritos da Lizzie é sua falta de recursos. Vou ilustrar isso com uma lista parcial das coisas que a Lizzie não consegue fazer. A Lizzie não consegue:

  • ler ou gravar a partir do seu sistema de arquivos
  • executar consultas SQL
  • solicitar sua senha
  • alterar algum estado do computador

Na verdade, um trecho de código da Lizzie pronto para uso não pode ser mal-intencionado, nem mesmo teoricamente! Essa falta de recursos proporciona à Lizzie algumas capacidades exclusivas que os scripts Roslyn e C# não proporcionam.

Em seu estado original, a Lizzie é completamente segura, permitindo que você transmita o código por uma rede com segurança, de um computador para outro, assim como você usaria o JSON para transmitir dados. Em seguida, no seu ponto de extremidade que aceita código Lizzie, você precisaria, explicitamente, implementar suportes para quaisquer funções às quais o código Lizzie precise ter acesso para dar certo em seu caso de uso. Isso pode incluir um método C# que leia dados de um banco de dados SQL, a capacidade de atualizar dados em um banco de dados SQL ou mesmo ler ou gravar arquivos. No entanto, todas essas invocações de função podem ser postergadas até que você tenha certeza de que o código tem permissão para fazer aquilo que ele está tentando fazer. Dessa forma, você pode implementar a autenticação e autorização facilmente antes de dar permissão ao código para, por exemplo “inserir sql”, “ler arquivos” ou qualquer outra coisa.

Essa propriedade da Lizzie permite que você crie um ponto de extremidade REST HTTP genérico, no qual a camada de cliente envia o código Lizzie ao seu servidor, onde ele é avaliado. Depois, você pode fazer com que seu servidor crie uma resposta JSON que ele envia de volta para o cliente. E, de modo ainda mais interessante, você pode implementar isso com segurança. Você pode implementar um único ponto de extremidade REST HTTP que aceite apenas as solicitações POST que contenham o código Lizzie e, literalmente, substituir seu back-end inteiro por um avaliador de Lizzie totalmente dinâmico e genérico. Isso permite que você mova toda a lógica de negócios e a camada de acesso de dados para seu código de front-end, de modo que ele crie dinamicamente o código Lizzie transmitido ao servidor para ser avaliado. E você pode fazer isso de modo seguro, desde que autentique e autorize os clientes antes de permitir que eles avaliem o código Lizzie.

Basicamente, de repente seu aplicativo inteiro é facilmente criado em JavaScript, TypeScript, ObjectiveC etc., e você consegue criar clientes em qualquer linguagem de programação desejada que crie dinamicamente o código Lizzie, enviando-o ao seu servidor.

Lições de Einstein

Quando Albert Einstein escreveu sua famosa equação para explicar nosso universo, ele tinha três componentes simples: energia, massa e a velocidade da luz ao quadrado. Essa equação poderia ser facilmente entendida por qualquer adolescente de 14 anos de idade com uma boa noção de matemática. Entender a programação de computadores, por outro lado, requer milhares de páginas e milhões ou mesmo trilhões de palavras, acrônimos, tokens e símbolos, além de uma seção inteira da Wikipédia sobre paradigmas, diversos padrões de design e uma variedade de linguagens, cada uma com estruturas e ideias completamente diferentes que requerem estruturas e bibliotecas “essenciais” que você precisa adicionar à sua “solução” antes de começar a trabalhar em seu problema de domínio. Além disso, espera-se que você conheça todas essas tecnologias de cor antes de poder ser chamado de desenvolvedor de software intermediário. Será que sou o único que enxerga o problema aqui?

Nem a Lizzie nem os delegados simbólicos são uma receita mágica, mas certamente são uma etapa na direção da “20 GOTO 10”. E, às vezes, para seguir em frente, você precisa começar dando um passo para trás. Algumas vezes, você precisará observar de fora, de modo neutro. Se nós, como uma comunidade profissional, fizermos isso, poderemos acabar percebendo que a cura para nossa loucura atual é a simplicidade, e não criar mais 50 padrões de design, 15 novas linguagens, 100 novos recursos de linguagem e um milhão de novas bibliotecas e estruturas, cada uma com um trilhão de peças móveis.

Menos é mais, sempre! Então, precisamos de mais menos e menos mais. Se concorda com essa ideia, junte-se a mim em github.com/polterguy/lizzie. Aqui você encontrará a Lizzie, com zero segurança de tipos, nenhuma palavra-chave, nenhum operador, nenhum OOP e definitivamente não mais que uma única biblioteca ou estrutura visível.

Conclusão

Grande parte do setor de computação tende a não concordar com a maioria das minhas ideias. Se você perguntar ao arquiteto médio de software o que ele pensa sobre essas ideias, provavelmente ele geraria bibliotecas inteiras na sua frente para provar que eu estou errado. Por exemplo, a suposição predominante no desenvolvimento de software é que a tipagem forte é boa e tipagem fraca é ruim. Para mim, contudo, manter a simplicidade é a única saída, mesmo que seja preciso descartar a tipagem forte. Lembre-se, no entanto, que ideias como a Lizzie têm o objetivo de “incrementar” seu código C# estaticamente tipado atual, e não o substituir. Mesmo que você nunca use diretamente as ideias de codificação apresentadas neste artigo, entender os conceitos principais pode ajudar você a escrever um código padrão em uma linguagem de programação tradicional de modo mais eficiente, além de ajudar a atingir a simplicidade.

Uma lição sobre a história da programação

Quando eu era um desenvolvedor júnior, costumava criar aplicativos de três camadas. A ideia era separar as camadas de dados das camadas da lógica de negócios e das camadas da interface do usuário. O problema é que, conforme as estruturas de front-end se tornam mais complexas, você é forçado a criar uma arquitetura de aplicativo de seis camadas. Primeiro, você precisa criar uma arquitetura de três camadas do lado do servidor e, depois, uma arquitetura de três camadas do lado do cliente. E, como se isso não bastasse, você precisa portar seu código para Java, ObjectiveC etc., para dar suporte a todos os clientes possíveis. Desculpe por ser contundente aqui, mas esse tipo de arquitetura é o que chamo de “design voltado à insanidade”, uma vez que ele aumenta a complexidade dos aplicativos até um ponto em que a manutenção seja praticamente impossível. Geralmente, uma única alteração na interface do usuário de front-end se propaga por 15 camadas de arquitetura e quatro diferentes linguagens de programação, forçando você a fazer alterações em todas essas camadas para adicionar uma simples coluna em uma grade de dados do front-end. A Lizzie resolve essa questão ao permitir que o front-end envie o código para seu back-end, que o avalia e o retorna ao seu cliente como JSON. É claro que você perde a segurança de tipos, mas ela acaba não valendo a pena quando, para mantê-la, é preciso unir as diferentes camadas dos aplicativos, de modo que as alterações em um local se propaguem para todas as outras camadas no seu projeto.

Eu comecei a codificar aos 8 anos de idade, em 1982, usando um Oric 1. Eu me lembro claramente do primeiro programa de computador que escrevi. Era assim:

10 PRINT "THOMAS IS COOL"
20 GOTO 10

Se hoje uma criança de 8 anos quiser reproduzir essa experiência seguindo todas as melhores práticas, usando uma estrutura do lado do cliente, como Angular, e uma estrutura de back-end, como .NET, ela provavelmente precisaria saber de cor milhares de páginas da literatura técnica da ciência da computação. Por outro lado, eu comecei com um livro que tinha mais ou menos umas 300 páginas e um punhado de revistas sobre computação. Antes de chegar na página 100, eu já tinha criado meu próprio jogo de computador com 8 anos de idade. Sei que isso me faz parecer velho, mas isso não é evolução e melhoria, é uma “involução” e uma loucura. E, sim, antes de você começar a escrever suas objeções freneticamente, esse problema já foi pesquisado cientificamente e observado e confirmado, com uma visão neutra, por professores e doutores, todos mais inteligentes que eu.

A verdade é que a programação de computadores como uma profissão (humana) está na iminência de ser extinta. Isso porque a complexidade que está sendo continuamente adicionada a ela poderá, em breve, superar a capacidade do cérebro humano de criar programas de computador. Pelo fato de a programação estar se tornando muito exigente em termos cognitivos, pode ser que nenhum ser humano seja capaz de realizá-la daqui uns 10 anos. Ao mesmo tempo, os seres humanos estão se tornando cada vez mais dependentes dos computadores e software a cada dia que passa. Pode me chamar de ultrapassado, mas eu gosto da ideia de haver um ser humano em algum lugar por aí capaz de entender o que é essencial para minha felicidade, existência e qualidade de vida.


Thomas Hansen desenvolve software desde os 8 anos de idade, quando começou a escrever código usando um computador Oric-1 em 1982. Ele se autointitula um programador de computadores zen que busca diminuir a complexidade da programação moderna e se recusa a promulgar qualquer crença nos dogmas tecnológicos. Hansen trabalha para a Bright Code, em Chipre, onde cria software FinTech.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey


Discuta esse artigo no fórum do MSDN Magazine