Site menu A vingança de Turing

A vingança de Turing

No texto sobre LISP cito a décima regra de Greenspun. Como hoje em dia é trivial e socialmente aceitável embutir um interpretador Python, JS ou Lua num programa escrito em C/C++ — a brazuca Lua foi desenvolvida especialmente para isso — chuto uma possível adaptação dessa regra para os dias de hoje: todo sistema suficientemente complexo escrito em linguagem compilada vai acabar embutindo uma VM de linguagem interpretada, e partes crescentes do código serão escritas ou reescritas nessa linguagem interpretada.

O motivo disso é a barreira código-dados, muito grossa numa linguagem compilada. As linguagens interpretadas, além de lidarem mais facilmente com os tipos de dados relevantes para humanos, permitem especificar comportamento de forma fluida. Alterar um algoritmo de decisão implica apenas em mexer num arquivo-texto, enquanto antes envolvia compilação. Do ponto de vista da máquina, arquivos-texto são estritamente dados, mas a VM permite que também sejam código.

Mas existe outra barreira análoga: entre os programas e suas respectivas configurações.

Quem é do ramo do UNIX/Linux sabe que existem centenas, quiçá milhares de "linguagens de configuração", tecnicamente chamadas de DSLs (linguagens específicas de domínio). Praticamente cada arquivo no /etc ostenta uma DSL diferente. Quem é antigo deve lembrar da bizarra e folclórica configuração do sendmail.

No mundo Windows existe historicamente uma padronização em torno do Registry e do formato .INI. Por uma época o XML estava na moda, era a DSL que aposentaria todas as outras. Hoje em dia há uma tendência universal de adotar JSON e principalmente YAML.

A ideia de um arquivo ou registro de configuração é influenciar o comportamento de um programa sem ter de alterá-lo e/ou recompilá-lo, dentro de certos limites. O primeiro instinto do desenvolvedor é oferecer um esquema de configuração o mais simples e limitado possível, não só porque é mais fácil de implementar, mas também porque impede o usuário de dar um tiro no pé. Se você dá poder demais a seus usuários, eles vão fazer bobagem, e dependendo da relação comercial isso cria um problema de suporte.

Assim sendo, a DSL de configuração de qualquer programa tende a nascer não-Turing-completa. Ou seja, ela não é capaz de expressar um algoritmo; ela é menos poderosa do que uma linguagem de programação "normal". Mas, como o desenvolvedor já sofreu com as limitações de outras DSLs de configuração, não raro ele inventa uma DSL nova, que pretensamente dá toda a flexibilidade necessária, mas ainda cuidando de deixar Turing de fora.

Por outro lado, o Santo Graal de configuração Turing-completa seria mexer diretamente no código-fonte. Mexendo no fonte, você pode absolutamente tudo, inclusive implementar features faltantes. Isto não é prático num programa grande, complexo e escrito em linguagem compilada. Mas é praxe em scripts pequenos. O /etc do Linux está cheio de scripts que possuem caráter não-binário, são programas e configurações ao mesmo tempo.

Todo sistema suficientemente complexo e longevo acaba tendo de aprimorar sua DSL de configuração, deixando-a cada vez mais perto de ser Turing-completa. Num primeiro momento o desenvolvedor vai adicionar mais e mais diretivas de configuração para cobrir todos os casos fortuitos, até chegar o ponto em que ele vê que é mais fácil deixar Turing entrar, e cada usuário que se resolva sozinho.

Um exemplo já citado, embora antigo, é o sendmail. A fim de suportar todos os casos fortuitos de manipulação de e-mail, a DSL de configuração virou um monstro, responsável pela morte do produto. Todo mundo migrou para o Postfix, cuja configuração é bem mais palatável, embora também padeça da falta do Turing.

Um exemplo de DSL de configuração Turing-completa é a do servidor Web NGINX. Certamente ela não era Turing-completa desde o nascimento. O resultado é uma DSL que até funciona, mas não se parece com nada.

Tanto na configuração do NGINX quanto do Postfix, quando deparamos com uma situação complicada, tipo redirecionamento de páginas ou de emails segundo regras complexas, temos de ficar procurando receitinhas de bolo na Internet. Se as configurações fossem escritas em linguagem de programação convencional, e pudéssemos codificar nossas próprias regras de redirecionamento, seria muito mais fácil — pelo menos para quem sabe programar.

Um exemplo de DSL não-Turing-completa é a do systemd. Apesar do systemd funcionar bem e ter substituído o velho init em todas as distros Linux importantes, considero que se perdeu alguma coisa no processo. Eu ainda preferia a inicialização baseada em scripts às inúmeras diretivas de configuração. Sempre há algum caso fortuito em que um comportamento desejado X, que poderia ser implementado em script, não é suportado no systemd ou só existe a partir da versão x.y.z que obviamente é muito mais nova que a versão do systemd que roda na sua máquina...

Ainda outro exemplo é a configuração de rede no Linux, que tem uns 10 sistemas diferentes: NetworkManager, NetPlan, etc. Eles fazem um esforço honesto, até funcionam razoavelmente bem numa instalação baunilha, ou rodando num notebook onde a conexão Wi-Fi é fugaz. Mas, num servidor, uma configuração suficientemente complexa que envolva roteamento por política ou QoS acaba sempre pedindo um script, com a complicação adicional de compatibilizar o script com as atividades do configurador de rede, para que não interfiram. Ainda sou mais fã do velho sistema BSD onde um script configura a rede, com chamadas diretas a ifconfig e tudo mais.

Um compromisso adotado por muitos e muitos programas, em geral com sucesso, é adicionar "ganchos" ou "triggers" opcionais, onde invoca-se um script ou programa externo quando um certo evento acontece. Se você quer emitir uma mensagem MQTT para um certo evento, e o programa principal não suporta MQTT, mas permite invocar um script externo, eis a sua válvula de escape. A vantagem do gancho rodar um programa externo é a isolação: se o gancho tiver algum problema, não afeta o programa principal. E se o gancho não atinge o objetivo, o usuário sabe que é problema dele.

Custos

Claro que admitir que a configuração seja Turing-completa traz desafios. É preciso tratar erros de programação de forma graciosa, desde erros de sintaxe até programas que entram em loop infinito. É necessário escolher a linguagem e embutir sua respectiva VM, o que abre outra celeuma: sempre vai ter aquele zé-ruela que vai criticar, dizer que a linguagem INTERCAL era melhor, etc.

A velocidade dessa linguagem tem de ser suficiente para a tarefa e.g. se um servidor Web delega o redirecionamento de páginas para uma linguagem Turing-completa, elta tem de ser rápida o suficiente para não prejudicar a vazão, ou então deve existir uma estratégia de cache dessa decisão, etc.

Se, por exemplo, uma configuração qualquer é fornecida na forma de uma função em vez de uma constante, quando esta função deve ser avaliada? A cada ocasião em que esta configuração é consultada? Ou apenas uma vez no início do programa? E se a configuração é utilizada milhares de vezes por segundo? São questões que têm de ser pensadas. E documentadas, porque metade dos usuários vai presumir que é de um jeito, e a outra metade vai presumir que é de outro.

O amigo LVR lembrou de uma possível distinção em outra dimensão: arquivos de configuração "passivos" versus "ativos" i.e. ambos poderiam ser expressos em linguagem Turing-completa porém os primeiros seriam avaliados apenas uma vez.

Em suma, suportar configuração na forma de uma linguagem Turing-completa tem custos. Não é para todo mundo. É algo a se considerar quando uma DSL não-Turing-completa custa demais para manter e ainda não atende a todos.

Por mais que eu ache que tentar adotar uma linguagem palatável ao mítico usuário final não-programador seja um tiro no pé, isso não é motivo para adotar uma linguagem difícil para expressar configuração. Uma linguagem simples e pequena vai melhor. Tcl e principalmente Lua foram criados com esse tipo de uso em mente, e acho que vão muito bem. Minhas próximas opções seriam JS e Python, nesta ordem, mas são linguagens bem maiores e aí já começaria a competição com outras linguagens, e com os respectivos defensores apaixonados que cortariam os pulsos por Ruby, Elixir, LISP etc.

Relação com ferramentas no-code e low-code

Desde que comecei a trabalhar com informática, no final dos anos 1980, ouve-se falar das ferramentas low-code e no-code, e do pretenso impacto que elas teriam em nossa profissão. SQL, CASE, 4GL, UML, todas essas tecnologias aí prometeram (ou induziram alguém a prometer) que qualquer mané podia ser programador e isso em breve seria trabalho de secretária.

Acho que podemos fatiar essa discussão em algumas vertentes.

Tem o pessoal que acha que no-code/low-code vai aposentar o Sr. Turing, ou seja, tudo poderia ser expresso por linguagens declarativas, não-Turing-completas. Assim sendo, no-code vai aposentar os programadores de todo o gênero.

Ora, neste texto pretendemos denunciar que linguagens não-Turing-completas não são suficientes nem mesmo para expressar a configuração de um sistema. Quanto mais o sistema em si...

Tudo bem deixar Turing de fora quando isso é possível. Um bom programador usa autômatos finitos ou redes de Petri sempre que pode. Mas às vezes não tem como.

Em cima disso, vem a über-promessa: que essas linguagens declarativas não-Turing-completas seriam tão fáceis que qualquer mané seria capaz de expressar regras de negócio com elas. (Mesmo que tais linguagens existissem, poderia então acontecer de elas serem ainda mais difíceis que as convencionais.)

A segunda vertente é consoante com o que colocamos na primeira parte deste texto: todo sistema grande, se evolui de forma saudável, tende a ser cada vez mais "scriptável". Isto é um sabor de low-code, e perfeitamente válido. Permite, por exemplo, que alguém desenvolva uma fase de um jogo sem ser necessariamente um programador de jogos puro-sangue (o que implicaria em conhecer OpenGL, álgebra linear, C++, OpenGL, GPUs, etc.)

A terceira vertente é uso de ferramentas gráficas para tornar o low-code/no-code acessível a "leigos". As planilhas eletrônicas são um exemplo bem-sucedido. Pode-se escrever rotinas em linguagem Turing-completa dentro do Excel, porém é possível passar sem Turing na maior parte do tempo. Isso é uma faca de dois gumes. Por um lado, permite que milhões de pessoas administrem sua vida e seus negócios sem a ajuda de ninguém. Por outro, viabiliza a criação de monstros no estilo da "Planilha do Praxedes".

Um contra-exemplo é o MS-Access, onde a promessa era permitir a um leigo desenvolver sistemas inteiros à base de cliques de mouse. O Access é um grande produto, porém cai num limbo onde os fundamentos não são simples o suficiente para um não-programador. Os melhores "cases" de uso bem-sucedido de Access que vi foi na mão de programadores de verdade.

A quarta vertente é a promoção de linguagens de programação funcionais/declarativas. Isso é válido, na medida em que não se caia no mesmo engodo das linguagens 4GL: "ah, a linguagem vai ser tão facinha que qualquer mané consegue programar". Linguagens declarativas são tão ou mais difíceis que linguagens imperativas; a diferença é que em alguns domínios elas expressam melhor a ideia ou algoritmo desejado.

Um exemplo é o SQL. SQL é uma linguagem declarativa ou "cálculo relacional": você descreve o que você quer do banco de dados, não como. Já uma linguagem imperativa faria uma consulta ao banco segundo a "álgebra relacional", ou seja, especificando exatamente como navegamos por cada tabela, especificando quais índices usar, etc. do mesmo jeito que fazíamos em COBOL ou em Clipper.

Muita gente pirou na batatinha, achando que colocar SQL na mão do usuário final aposentaria os programadores. SQL é difícil até para programadores, imagine para usuários finais! A ideia de interfacear com um banco de dados relacional usando uma linguagem declarativa é muito boa, tanto que "pegou". Mas isso não torna SQL fácil, nem isenta totalmente o usuário-programador de lidar com problemas de álgebra relacional e.g. quando uma consulta ficou muito lenta porque o otimizador do banco pisou na bola.

Outro exemplo de uso válido de linguagens declarativas e não-Turing-completas, é ao lidar com Big Data. Quando se fala de Big Data, assuma que o volume de dados tende ao infinito, então é inviável o acesso a amostras individuais, muito menos rodar um algoritmo em cima delas. Você só tem acesso a agregações estatísticas: média, desvio-padrão, mediana, quantis, etc. Os dados têm de falar por si. Se não falam, existe uma deficiência de observabilidade que vai ser sanada por mais ou melhores dados, não por algoritmos.

Essa linha de pensamento inspira projetos como o Pandas que trata dados de forma declarativa apesar da linguagem Python ser imperativa.