Ha quase 2 anos que não publico um post … é tá complicado arranjar um tempo, e um assunto. Faz tempo que tenho vindo a usar qualquer momento livre para dedicar ao estudo de compiladores e linguagens de programação.
Tudo começou com a criação de um compilador. Foi interessante estudar este assunto apenas com os poucos recursos da internet. Comprei dois livros sobre o assunto ( sim, li o livro do dragão), mas são muito pouco úteis para iniciantes. Depois que você sabe, ai faz sentido, mas ai é tarde demais porque você já sabe o que o livro tenta ensinar. Além disso eu não queria um compilador qualquer. Não queria mais um YACC. Queria algo em java em que a sintaxe fosse programável o o resto viesse pronto. Isto deu nascimento ao compilelittle. É um compilador bottom-up capaz de compilar bastantes tipos de sintaxe e auto-resolver conflitos. Porque é genérico talvez não seja opção para algo que precise de performance, mas tem a vantagem de fazer parse de sintaxes que outros compiladores mais rápidos não fazem. As escolhas envolvidas no compilelittle tentaram seguir o state-of-the-art o mais próximo possivel. Li vários papers e livros sobre o assunto que sim ajudaram a entender mas o que ajudou mesmo foram as palestras deste senhor .
Com o compilador mais ou menos pronto queria pô-lo à prova. Tanto em termos do que ele é capaz de parsear, mas também na extensibilidade e configurabilidade do framework. Afinal, a ideia é que as pessoas incluam o compilelittle nos seus próprios projetos. Para isto precisa criar uma linguagem um pouco mais complexa. Esta linguagem é a linguagem lense
Criar a minha linguagem é uma experiencia bem divertida para mim embora alguns dias seja frustrante porque o esforço é imenso. Não porque é difícil, mas porque tenho pouquíssimo tempo para mexer com isso. Mas criar uma linguagem não é bem sobre criar um compilador. É sobre criar regras, estilos : design. Desenhar uma linguagem é ainda mais interessante que criar o compilador dessa linguagem. Como você cria uma linguagem ?
Uma forma de fazer é simplesmente inventar uma linguagem e pronto. Esta forma de fazer é historicamente mais utilizada quando ha um problema a resolver. Esta forma tenta resolver um problema especifico. A outra forma de fazer é estabelecer princípios e derivar a linguagem deles. Como físico, obviamente, esta segunda forma me interessa mais. Por outro lado, vivemos uma época áurea para as linguagens de programação. Não ha como escapar da influencia de tantas linguagens por ai, como por exemplo: Java 8 , C# 7, Swift 3, Scala, Julia, Python, Javascript, TypeScript 2, Ceylon, Kotlin, Gosu e Fantom.
A primeira parte obvia é que a linguagem deveria correr dentro de uma JVM e ser fortemente tipada. Já foi provado que VM, e em especial a JVM são capazes de performance igual ao superior a programas compilados estaticamente. A performance da JVM cresce a cada nova versão e é a VM mais avançado do mercado. Não é por acaso que a maior parte das linguagens modernas rodam na JVM. Por outro lado, mesmo as linguagens estaticamente compiladas como Julia ou Swift já fazem uso de um tipo de compilação em duas fases usando a LLVM ( pese embora o nome não é bem um VM, mas um standard de compilação estática que suporta objectos). A segunda parte obvia para mim é que não basta rodar na JVM, tem que rodar dentro do browser. A forma mais simples hoje em dia é compilar para javascript como fazem tantas outras linguagens como TypeScript , Kotlin, Ceylon e Fantom. Chega de projetos esquizofrênicos que falam várias línguas. Na real, na prática, isso pode ser interessante para provas de conceito ou alavancas de start-ups que jogam o código fora sempre que ha dinheiro novo. Eu acho que é uma falácia que desenvolvimento rápido significa desenvolvimento descartável e mal feito. Claro que você pode criar sua start-up com javascript e node.js. O ponto é que não é a escolha mais correta do ponto de vista econômico a longo prazo. É necessária uma tecnologia que possa produzir diferentes artefatos a partir do mesmo código; o que nos leva ao Android. Hoje é preciso compilar para .class para usar a tecnologia android, embora Android não rode java. Os .class são convertidos para .dex e transformados em aplicações. Linguagens como Kotlin já provaram que ha vantagens em ter uma só base de código, uma só linguagem, para manipular diferentes APIs em diferentes ambientes. É este tipo de linguagem que me interessa investigar. Uma linguagem OO, fortemente tipada, coesa que permite ter uma só base de código em várias plataformas. As plataformas principais são Java e Javascript, mas se bem feito, não seria difícil estender para .NET e Android e quiça até iOS usando a LLVM também.
Porquê uma linguagem com tipagem forte ? Porque tipos tornam a linguagem mais segura e mais coesa. Ou seja, o uso de tipos significa menos bugs, menos testes e mesmos surpresas. Como as pessoas pensam em tipagem forte pensam em Java onde é preciso declara os tipos a todo o momento. O que as novas linguagens mostram é que, porque o código é sempre fortemente tipado, o compilador pode inferir os tipos a maior parte das vezes. Na realidade ninguém quer que o compilador infira o tipo de uma propriedade ou dos parâmetros de um método. O que se quer é que dados esses tipos ele infira todos os outros tipos dentro dos métodos. Inferência de tipos torna o código mais simples de escrever, e muitas vezes acaba sendo o código que escreveria em linguagens não tipadas, mas sem abdicar da ajuda imensa que o compilador nos dá durante a verificação de tipos. Porque os tipos são inferidos durante a declaração de variáveis o tipo deve ser optional e isto nos leva a ter que decidir a sintaxe de forma a que seja natural pôr ou não o tipo. Por isso que em lense os tipos vêm depois do nome da variável, para que possam removidos quando a inferencia está sendo usada.
Outra coisa que queria ter na linguagem é um conjunto coerente de tipos numéricos. Isto tem mais que ver com o design de API, mas tem que haver suporte da linguagem em alguns pontos. Durante o projeto MiddleHeaven é que o tamanho de uma colação não deve ser limitado pelo limite do tipo numérico usando . Em java é int, o que significa que nunca poderemos ter coleções maiores que o número de inteiros positivos. Em C# existe um tamanho em int e um em long e o de long é que é considerado “o verdadeiro”, embora na prática uma máquina vai ter problemas em aguentar mais que um int. Mas isso é hoje. No futuro as máquina podem ter muito mais memória e aguentar muito mais valores. Por outro lado, int e long têm valores negativos. É absurdo que o tamanho de uma coleção – a contagem de elementos dentro da coleção – seja negativo. Portanto, lense conta com o tipo Natural que é sempre positivo e serve para contar coisas de zero até o tamanho que a máquina aguentar. Números sem limite máximo imposto pela linguagem são muito uteis como provam o BigDecimal e BigInteger do java, mas a linguagem tem que permitir usar tipos desses mais facilmente.
Interoperabilidade é outra questão importante. É ao mesmo tempo importante poder utilizar tecnologia nativa à plataforma base como por exemplo JDBC em Java e JQuery em javascript mas sem que isso inunde seu código e o torne todo o sistema não portátil. Esse tipo de interação deve ser possível mas restringida. Por outro lado, se recebemos um objecto de uma biblioteca escrita na linguagem nativa da plataforma, tem que ser possível manipulá-lo diretamente. Manipular aqui significa invocar métodos e ler campos. Contudo, idealmente, o código deve ser escrito em lense puro e apenas algumas partes serem escritas na linguagem base da plataforma. Isto também significa que é necessário prover um conjunto de classes base como String, Boolean, Number e outras para o programador não tenha que constantemente lançar mão dos tipos nativos. Ceylon é uma linguagem que vai por este caminho. Kotlin parece ir por este caminho, mas na realidade está fortemente ligada aos tipos nativos dos java e o programador não tem que fazer nenhum esforço para os usar, o que torna o código não portável muito facilmente (mas permite usar o Kotlin como um Java++). Por outro lado, o estilo Ceylon torna a criação de aplicações mais complexas um pouco mais demorado porque várias tipos de bibliotecas precisam ser recriadas em Ceylon.
Para que a base de código seja realmente reaproveitável é necessário este isolamento. O que nos leva ao conceito de módulo. Módulos são uma coisa nova no java 9, mas existem de raiz em outras linguagens como Ceylon e TypeScript. Eles realmente ajudam a organizar o código e de quebra ainda permite controlar que certos módulos só podem ser usados em certos ambientes.
Design de linguagens é bem interessante, mas descobri que demanda um certo conhecimento caso contrário erros no design vão ser cometidos que levam à inconsistência. É necessário também uma distinção entre o que o compilador pode fazer mecanicamente e o que a linguagem pode fazer teoricamente. Em java , por exemplo, o compilador tem que entender intersecção de tipos porque é possível escrever:
public <T extends Serializable & Clonable> T copy(T obj)
Tudo bem que apenas podemos usar intersecção de tipos em genéricos, mas isso não importa para o compilador. Se ele precisa tem a lógica para saber lidar com esses tipos não importa muito onde a linguagem define que eles podem ser usados. Em Ceylon, por exemplo podemos escrever :
copy (Serializable & Clonable obj)
Sem usar tipos genéricos e o compilador vai entender exatamente a mesma coisa.
O design da linguagem , e ainda mais uma que é fortemente tipada, depende fortemente de desenha um sistema de tipos (type system). Coisas como intersecção de tipos, união de tipos, tipos e algébricos são alguns conceitos que começam a aparecer em bastantes linguagens e tornam as coisa mais seguras sob pena de obrigar o programador a saber mais conceitos. Java é um bom exemplo de como aumentar o número de conceitos necessários para começar a linguagem não é um problema a longo prazo. Java obrigou pensamento OO em um tempo onde isso não era exigido. Hoje ele conta com conceitos como genéricos, tipos de intersecção (embora limitado) , monads (Streams, Optional, CompletableFuture) , traits (as novas interfaces do java 8 em que se podem definir métodos são traits), etc.. Algumas coisas passam até desapercebidas ao programador de hoje, mas jogar o java 9 nas mãos de um programador em 1990 em vez do java 1.0 teria sido uma receita para o abandono da linguagem. Então, hoje, as novas linguagens têm que introduzir conceitos novos, mas de um jeito que seja simples e faça sentido, mesmo quando você não conhece toda a teoria por detrás daquilo. E isso é um desafio de design.
Sérgio, parabéns pelo artigo.