O Taq postou um twitter sobre os custos da fragmentação das linguagens de programação. Apesar de comum em diversas atividades humanas, o cara tem uma certa razão. O desperdício de tempo de computador, energia elétrica, ideias, etc. é grande. Muitas vezes por nada. Talvez trocar seis por meia dúzia. Atualmente tenho visto uma movimentação em torno de Rust. Os caras estão desenvolvendo até um emacs em Rust. Como se não bastasse as “novas” linguagens disponíveis, temos o problema das plataformas. Linux, Windows, Mac, Android, iOS. Tudo fragmentado. Aí o cara escolhe electron e baixa a metade do HD para desenvolvimento. O programa ocupa a outra metade. E o cara ainda tem coragem de usar uma camiseta escrita “Save the planet”. Pelos materiais utilizados, temos até coleta especial para lixo eletrônico.
Então eu releio o texto antigo do Carl Sanssenrath e penso…
To me this is all about Personal Computing, not Personal Enslaving. It is about being the masters of our own computers, not the reverse.
Para lutar contra a complexidade do desenvolvimento ele criou REBOL. É, uma linguagem muito avançada para a época. Tipo Smalltalk e seu ambiente gráfico com janelas, menus e mouse para a década de 80. Acho que continua muito avançada para os dias de hoje (ou para os programadores de hoje). Sendo uma linguagem proprietária e como o desenvolvimento estava estagnado, Nenad Rakocevic, iniciou a criação de Red. Basicamente a mesma linguagem com algumas melhorias como abranger todo o espectro do desenvolvimento indo do baixo nível (C,…), passando por níveis mais elevados (Ruby, Python,…) e indo até a criação de DSLs (a própria linguagem possui diversas).
Eliminando a complexidade
- Como abrange todos os níveis de programação e a sintaxe é a mesma, não é necessário que alguém programando em Ruby, Python, etc. tenha que aprender C para adicionar uma funcionalidade disponível apenas neste ambiente. Até o final do artigo veremos como.
- Como permite, sem nada adicional, gerar executáveis para os diversos ambientes (Linux, Windows, Mac, etc..) e sem a necessidade de trocar de ambiente, evita que o programador tenha que ficar trocando de SO para ver se está tudo bem (basta executar o programa em outro SO para ver se está tudo bem).
- Para a interface gráfica existe o dialeto VID que, com as mesmas linhas de programação gera GUI para os sistemas desejados sem a necessidade de baixar bibliotecas adicionais. Nada impede que se adicione a possibilidade de criar uma TUI usando o curses ou algo do tipo. A linguagem possui diversas DSLs escritas nela mesmo.
- Tudo o que foi dito apenas baixando aproximadamente 1Mb e sem precisar instalar nada.
Mas vejamos alguns aspectos na prática.
Vamos começar escovando bits e trabalhar diretamente com o sistema operacional. Como uso Linux, deixarei os outros sistemas operacionais de fora pois nem tenho como testar. \mas nada impede de fazer o mesmo com os outros. Vejamos como ficaria para imprimir uma string usando uma chamada direta ao Linux.
Syscall
Red/System [] ; definições globais #define STDOUT 1 #define SYS-write 4 #define SYS-exit 1 #define null-byte #"^@" #syscall [ write: SYS-write [ fd [integer!] buffer [c-string!] count [integer!] return: [integer!] ] quit: SYS-exit [ status [integer!] ] ] ; retorna o tamanho de uma string terminada por zero length?: func [s [c-string!] return: [integer!] /local i][ i: 0 while [s/1 <> null-byte][ s: s + 1 i: i + 1 ] return i ] ; imprime uma string print: func[s [c-string!] return: [integer!]][ write STDOUT s length? msg ] ; programa msg: "Hello World^/" result: print msg if result > 0 [result: 0] quit result
Mesmo sem ter (poder utilizar) os cabeçalhos de C (.h), o programa possui apenas 40 LOC. Algumas definições globais, o protótipo das funções que serão utilizadas e o programa com a mesma sintaxe de Red. O nome da função para retornar o tamanho de uma string podia ser strlen ou size. Foi utilizado lenght? para seguir o que já existe na linguagem e não ter que aprender nada de novo. Para abstrair um pouco mais, foi criada a função print para ficar mais próximo do que já existe. Poderia colocar tudo que vem antes do programa em um arquivo separado e trocar por uma linha como #include syscall.h que nem daria para saber que não é um programa em Red. Procure por uma versão em C para ver como ficaria.
Ok. Agora vamos compilar, usando a chave –no-runtime para não incluir o runtime de red/system (na sequencia veremos mais sobre isso). O resultado é:
# guaracym @ guaracy-pc in ~/red/reds-examples $ red -c --no-runtime syscall.reds -=== Red Compiler 0.6.3 ===- Compiling /home/guaracym/red/reds-examples/syscall.reds ... Target: Linux Compiling to native code... ...compilation time : 19 ms ...linking time : 5 ms ...output file size : 624 bytes ...output file : /home/guaracym/red/reds-examples/syscall
Pois é. O resultado é um executável com abundantes 624 bytes. E que funciona perfeitamente.
# guaracym @ guaracy-pc in ~/red/reds-examples $ ./syscall Hello World
Apenas por curiosidade, vamos ver o código gerado.
# guaracym @ guaracy-pc in ~/red/reds-examples $ objdump -d syscall syscall: formato de ficheiro elf32-i386 Desmontagem da secção .text: 0804809c <.text>: 804809c: ff 35 38 91 04 08 pushl 0x8049138 80480a2: e8 69 00 00 00 call 0x8048110 80480a7: a3 4c 91 04 08 mov %eax,0x804914c 80480ac: a1 4c 91 04 08 mov 0x804914c,%eax 80480b1: 3d 00 00 00 00 cmp $0x0,%eax 80480b6: 7e 0f jle 0x80480c7 80480b8: b8 00 00 00 00 mov $0x0,%eax 80480bd: c7 05 4c 91 04 08 00 movl $0x0,0x804914c 80480c4: 00 00 00 80480c7: ff 35 4c 91 04 08 pushl 0x804914c 80480cd: 5b pop %ebx 80480ce: b8 01 00 00 00 mov $0x1,%eax 80480d3: cd 80 int $0x80 80480d5: 55 push %ebp 80480d6: 89 e5 mov %esp,%ebp 80480d8: 6a 00 push $0x0 80480da: 6a 00 push $0x0 80480dc: 83 ec 08 sub $0x8,%esp 80480df: b8 00 00 00 00 mov $0x0,%eax 80480e4: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%ebp) 80480eb: eb 0e jmp 0x80480fb 80480ed: 8b 45 08 mov 0x8(%ebp),%eax 80480f0: 40 inc %eax 80480f1: 89 45 08 mov %eax,0x8(%ebp) 80480f4: 8b 45 f0 mov -0x10(%ebp),%eax 80480f7: 40 inc %eax 80480f8: 89 45 f0 mov %eax,-0x10(%ebp) 80480fb: 8b 75 08 mov 0x8(%ebp),%esi 80480fe: 8a 06 mov (%esi),%al 8048100: 3c 00 cmp $0x0,%al 8048102: 75 e9 jne 0x80480ed 8048104: 8b 45 f0 mov -0x10(%ebp),%eax 8048107: e9 00 00 00 00 jmp 0x804810c 804810c: c9 leave 804810d: c2 04 00 ret $0x4 8048110: 55 push %ebp 8048111: 89 e5 mov %esp,%ebp 8048113: 6a 00 push $0x0 8048115: 6a 00 push $0x0 8048117: 6a 01 push $0x1 8048119: ff 75 08 pushl 0x8(%ebp) 804811c: ff 35 38 91 04 08 pushl 0x8049138 8048122: e8 ae ff ff ff call 0x80480d5 8048127: 50 push %eax 8048128: 5a pop %edx 8048129: 59 pop %ecx 804812a: 5b pop %ebx 804812b: b8 04 00 00 00 mov $0x4,%eax 8048130: cd 80 int $0x80 8048132: c9 leave 8048133: c2 04 00 ret $0x4
Ok. Encerramos por aqui. Deverá ser utilizado apenas em aplicações específicas e por profissionais qualificados. Não é para o nosso bico. Mas existe a possibilidade.
O nosso nível seria (acho eu), algo em um nível um pouco mais elevado, com o mesmo potencial de C mas um pouco mais bacana. Vamos supor um programa para gerar números aleatórios chamando as funções de uma biblioteca do sistema (no caso do Linux a libc). Ficaria algo assim:
Red/System
Red/System [] #import [ LIBC-file cdecl [ rand: "rand" [ return: [integer!] ] seed: "srand" [ seed [integer!] ] clock: "clock" [ return: [integer!] ] ] ] #define MAX 20 loop 2 [ seed 1 print-line ["--> seed 1" ] loop 5 [ print-line rand % MAX ] seed clock print-line ["--> clock" ] loop 5 [ print-line rand % MAX ] ]
Para o Linux, LIBC-file é definido como #define LIBC-file “glibc.so.6” (para o Windows é “msvcrt.dll”). Na definição dos protótipos, eu defini que seed no programa irá chamar srand da biblioteca. Assim é feita a compatibilidade entre os diversos SOs. O programa apenas demonstra que pode ser utilizado clock para inicializar a semente para os números aleatórios. Compilando as 34 LOC, temos:
guaracym @ guaracy-pc in ~/red/reds-examples $ red -c reds.reds -=== Red Compiler 0.6.3 ===- Compiling /home/guaracym/red/reds-examples/reds.reds ... Target: Linux Compiling to native code... ...compilation time : 381 ms ...linking time : 14 ms ...output file size : 7096 bytes ...output file : /home/guaracym/red/reds-examples/reds
O tamanho do executável ficou em 7096 bytes, o que pode ser considerado bem pequeno para os padrões atuais. O resultado da saída é:
# guaracym @ guaracy-pc in ~/red/reds-examples $ ./reds --> seed 1 3 6 17 15 13 --> clock 18 4 2 3 8 --> seed 1 3 6 17 15 13 --> clock 13 19 12 5 11
Em penúltimo lugar mas não menos importante, temos a linguagem Red propriamente dita. Um programa para ler uma página da internet, extrair o título e imprimir ficaria assim:
Red
Red [] p: read https://example.com parse p [thru <title> copy t to </title>] print ["O título da página é: " t]
Como tudo que vem antes de Red [] é considerado comentário, poderíamos ter algo como #!/usr/env red, alterar o atributo do arquivo para executável (chmod +x) e chamar direto como um arquivo de script qualquer. Ou poderemos chamar com red <nome do arquivo>. Poderia passar despercebido mas temos outro detalhe interessante. Foi utilizado parse que é uma DSL escrita em Red e substitui as expressões regulares. Continuamos com a mesma sintaxe da linguagem (e mais umas facilidades). Digamos que a leitura da página também é interessante. Nem precisa de baterias.
O resultado é:
# guaracym @ guaracy-pc in ~/red/reds-examples [17:31:04] $ red red.red O título da página é: Example Domain
Para enviar para um computador que não possua Red instalado(?), podemos gerar um executável e enviar apenas ele para o destino.
# guaracym @ guaracy-pc in ~/red/reds-examples [17:31:14] $ red -c red.red -=== Red Compiler 0.6.3 ===- Compiling /home/guaracym/red/reds-examples/red.red ... ...using libRedRT built on 25-Oct-2018/20:29:07 ...compilation time : 34 ms Target: Linux Compiling to native code... ...compilation time : 1487 ms ...linking time : 91 ms ...output file size : 40760 bytes ...output file : /home/guaracym/red/reds-examples/red
Aqui vale algumas observações:
- O tamanho informado é para desenvolvimento (compilado com -c). Para enviar para outro computador deverá ser compilado com a chave -r (release) e o tamanho ficará em torno de 600Kb ou mais.
- Na primeira vez, o tempo para a compilação é maior para gerar uma biblioteca que será utilizada (quase o mesmo com a opção -r).
- É possível gerar com a chave -t Windows e enviar diretamente para ser executado em um computador com Windows.
O resultado é o mesmo do chamado pela linha de comando (e nem poderia ser diferente).
# guaracym @ guaracy-pc in ~/red/reds-examples $ ./red O título da página é: Example Domain
Velocidade
Agora vamos juntar o que já foi visto até aqui. Supomos que desejamos um programa que calcule a sequência de Fibonacci de forma recursiva. Sabemos que não é a melhor forma mas suponhamos que e não existe outra. Utilizando uma linguagem que não seja compilada é impraticável para alguns valores mais elevados. Poderíamos utilizar uma biblioteca em uma linguagem compilada e mais rápida ou, podemos escrever em Red e compilar apenas a parte crítica. Ficaria assim:
Red [] ;; routine indica a compilação Red/System fib: routine [n [integer!] return: [integer!]][ either n < 3 [1] [(fib n - 1) + (fib n - 2)] ] ;; func rotina interpretada fibt: func [n][ either n < 3 [1] [(fibt n - 1) + (fibt n - 2)] ] print fib 40
Quando é encontrada a palavra routine, o interpretador sabe que necessita compilar aquela função. Então o programa não pode ser executado como script o que, na grande maioria dos casos, não deve ser problema nenhum. A maior diferença no programa está no fato de que na rotina compilada é necessário especifica o tipo dos parâmetros ao passo que na interpretada não é necessário. Mas nada impede de termos fibt: func [n [integer!]] onde o parâmetro também seria verificado na chamada e iria gerar um erro antes da execução se fosse chamado com uma string. É possível especificar mais de um tipo aceito pela rotina. Compilamos o programa e o resultado é:
# guaracym @ guaracy-pc in ~/red/reds-examples $ time ./fib 102334155 ./fib 0,83s user 0,00s system 99% cpu 0,834 total
Pareceu bom. Mas vamos testar com um programa em C para ver como fica.
#include<stdio.h> int fib(int n); int main() { printf("%d\n", fib(40)); } int fib(int n) { if (n<3) return 1; else return ( fib(n-1) + fib(n-2) ); }
# guaracym @ guaracy-pc in ~/red/reds-examples $ time ./fibc 102334155 ./fibc 0,65s user 0,00s system 99% cpu 0,653 total
Ok. O programa em C é mais rápido. Não é desculpa mas Red ainda é uma linguagem alfa (nem RC é) e o código não é otimizado. Acho que o resultado pode ser considerado bom. Independente do estágio da linguagem, vamos comparar com Ruby (só para deixar claro, Red interpretado é mais lento que Ruby)
def fib(n): if n==1 or n==2: return 1 return fib(n-1)+fib(n-2) print (fib(40))
# guaracym @ guaracy-pc in ~/red/reds-examples $ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux] # guaracym @ guaracy-pc in ~/red/reds-examples $ time ruby fib.rb 102334155 ruby fib.rb 18,45s user 0,01s system 99% cpu 18,460 total
A diferença no tempo de execução é significativa.
Então era isso. Apesar da abordagem muito superficial, acho que foi possível ter uma ideia da abrangência da linguagem e como tudo fica com uma forma parecida facilitando a vida do desenvolvedor. Não coloquei exemplo de VID pois a parte gráfica para o Linux ainda não está pronta. Basicamente um compilador multiplataforma, um interpretador e, tudo isso, em aproximadamente 1Mb. Nem é possível dizer que vem com as baterias. É mais uma usina.
Voltando ao início do artigo, é uma fragmentação que eu considero muito bem vinda e faço questão de compartilhar. Uma linguagem alfa também pode gerar alguns produtos alfa. Acho que o melhor exemplo é a carteira que está sendo desenvolvida, roda no Windows e Mac e tem só 300Kb sem dependências externas (quase surreal). Se você deseja alguns exemplos de scripts e programas feitos em Red, tem uma página com uma coleção de links. Você pode baixar Red para o seu SO. Só não baixe a versão estável (existe estável de alfa?). Vá até o final e baixe a última versão. Tem mais novidades do que erros.