De vez em quando, eu tropeço em uma linguagem de programação que faz algo tão diferente que muda como eu penso sobre a codificação. Nesta publicação, quero compartilhar algumas das minhas descobertas favoritas.
Esta não é a postagem do Blog da vovó “programação funcional que vai mudar o mundo!” : esta lista é muito mais esotérica. Eu aposto que a maioria dos leitores não ouviu falar sobre a maioria dos idiomas e paradigmas abaixo, então espero que você tenha tanta diversão aprendendo sobre esses novos conceitos como eu fiz.
Nota: Tenho apenas uma experiência mínima com a maioria dos idiomas abaixo: encontro as ideias por trás delas fascinantes, mas não reivindico experiência nelas, por isso, sinta atenção em todas as correções e erros. Além disso, se você encontrou novos paradigmas e ideias não cobertas aqui, compartilhe-as!
Concorrente por padrão
Exemplo de linguagens: ANI, Plaid
Vamos deixar as coisas com uma mente real dobrada: existem linguagens de programação por aí que são concorrentes por padrão. Ou seja, cada linha de código é executada em paralelo!
Por exemplo, imagine que você escreveu três linhas de código, A, B e C:
A;
B;
C;
Na maioria das linguagens de programação, A executaria primeiro, depois B, e depois C. Em uma linguagem como ANI, A, B e C, todos executariam ao mesmo tempo!
O fluxo de controle ou o pedido entre linhas de código em ANI é meramente um efeito colateral de dependências explícitas entre linhas de código. Por exemplo, se B tivesse uma referência a uma variável definida em A, então A e C seriam executados ao mesmo tempo, e B seria executado somente após A terminar.
Vejamos um exemplo na ANI. Conforme descrito no tutorial, os programas ANI consistem em “tubos” e “travas” que são usados para manipular fluxos e fluxos de dados. A sintaxe incomum é difícil de analisar, e o idioma parece morto, mas os conceitos são bastante interessantes.
Aqui está um exemplo de “Olá mundo” na ANI:
"Hello, World!" ->std.out
Na terminologia ANI, enviamos o “Olá, Mundo!” Objeto (uma string) para o fluxo std.out. O que acontece se enviarmos outra string para std.out?
"Hello, World!" ->std.out
"Goodbye, World!" ->std.out
Ambas as linhas de código são executadas em paralelo, para que possam terminar em qualquer ordem no console. Agora, veja o que acontece quando introduzimos uma variável em uma linha e fazemos referência mais tarde:
s = [string\];
"Hello, World!" ->s;
\s ->std<.out;
A primeira linha declara um “latch”(trava) (latches(travas) são um pouco como variáveis) chamado de s que contém uma string; A segunda linha envia o texto “Olá, Mundo!” Para s; A terceira linha “unlatches”(destrava) s e envia o conteúdo para std.out. Aqui, você pode ver a sequência implícita do programa escrito em ANI: uma vez que cada linha depende do anterior, este código será executado na ordem em que está escrito.
A linguagem Plaid também afirma suportar a concorrência por padrão, mas usa um modelo de permissões, conforme descrito neste artigo, para configurar o fluxo de controle. Plaid também explora outros conceitos interessantes, como a Programação Orientada a Tipo-Estado(Type-state Oriented Programming), onde mudanças de estado se tornam cidadão de primeira classe da linguagem: você define objetos não como classes, mas como uma série de estados e transições que podem ser verificados pelo compilador. Isso parece uma tomada interessante ao expor o tempo como uma construção de linguagem de primeira classe, como discutido em Rich Hickey. Estamos lá ainda falando.
O Multicore está em alta e a concorrência ainda é mais difícil do que deveria ser na maioria das linguagens. ANI e Plaid oferecem uma nova tomada sobre este problema que poderia levar a ganhos de desempenho incríveis; A questão é se “paralelo por padrão” torna a simultaneidade mais fácil ou difícil de gerenciar.
Atualização: a descrição acima capta a essência básica de ANI e Plaid, mas usei os termos “concorrente” e “paralelo” de forma intercambiável, embora tenham significados diferentes. Veja Concorrência não é paralelismo para mais informações.
Tipos dependentes
Exemplo de idiomas: Idris, Agda, Coq
Você provavelmente usava para sistemas em linguagens como C e Java, onde o compilador pode verificar se uma variável é um inteiro, uma lista ou uma string. Mas e se o seu compilador pudesse verificar se uma variável é “um número inteiro positivo”, “uma lista de comprimento 2” ou “uma sequência de caracteres que é um palíndromo”?
Esta é a ideia por trás de linguangens que suportam tipos dependentes: você pode especificar tipos que podem verificar o valor de suas variáveis em tempo de compilação. The shapeless library (A biblioteca sem forma) para Scala adiciona suporte parcial e experimental (leia: provavelmente não está pronto para o horário nobre) para tipos dependentes para Scala e oferece uma maneira fácil de ver alguns exemplos.
Veja como você pode declarar um vetor que contém os valores 1, 2, 3 com a biblioteca sem forma(shapeless library):
val l1 = 1 :#: 2 :#: 3 :#: VNil
Isso cria uma variável l1 que é assinatura de tipo especifica não só que é um vetor que contém Ints, mas também que é um vetor de comprimento 3. O compilador pode usar essa informação para detectar erros. Vamos usar o método vAdd em Vector para realizar uma adição pairwise entre dois vetores:
val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: 3 :#: VNil
val l3 = l1 vAdd l2
// Result: l3 = 2 :#: 4 :#: 6 :#: VNil
O exemplo acima funciona bem porque o sistema de tipo sabe que ambos os vetores têm o comprimento 3. No entanto, se tentarmos vAdd dois vetores de diferentes comprimentos, teríamos um erro no tempo de compilação em vez de ter que esperar até o tempo de execução!
val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: VNil
val l3 = l1 vAdd l2
// Result: a *compile* error because you can’t pairwise add vectors
// of different lengths!
Shapeless é uma biblioteca incrível, mas do que eu vi, ainda é um pouco áspera, só suporta um subconjunto de digitação dependente, e leva a um código relativamente detalhado e as assinaturas de tipo. Idris, por outro lado, faz dos tipos um membro de primeira classe da linguagem de programação, então o sistema de tipo dependente parece muito mais poderoso e limpo. Para uma comparação, veja as conversas de Scala vs Idris: dependentes, agora e no futuro.
Métodos de verificação formal têm sido em torno de um longo tipo, mas muitas vezes eram demasiado pesados para ser usado para programação de propósito geral. Tipos dependentes em idiomas como Idris, e talvez até Scala, no futuro, podem oferecer alternativas mais leves e mais práticas que ainda aumentam drasticamente o poder do sistema de tipo na captura de erros. Claro, nenhum sistema de tipo dependente pode capturar todos os erros devido a limitações inerentes do problema de parada, mas se bem feito, os tipos dependentes podem ser o próximo grande salto para sistemas de tipo estático.
Linguagens Concatenativas
Exemplo de linguagem: Forth, cat, joy
Já se perguntou o que seria programar sem variáveis e aplicação de função? Não? Nem eu. Mas, aparentemente, algumas pessoas fizeram, e eles apresentaram programação concatenativa. A idéia é que tudo na linguagem é uma função que empurra dados para uma pilha ou exibe dados fora da pilha; Os programas são construídos quase que exclusivamente através da composição funcional (a concatenação é a composição).
Isso parece muito abstrato, então vejamos um exemplo simples em cat:
2 3 +
Aqui, eu empurro dois números na pilha e, em seguida, chamar a função +, que aparece ambos os números da pilha e coloca o resultado de adicioná-los de volta para a pilha: a saída do código é 5. Aqui está um exemplo um pouco mais interessante:
def foo {
10 <
[ 0 ]
[ 42]
if
}
20
foo
Vamos caminhar por esta linha por linha:
- Primeiro, declaramos uma função foo. Observe que as funções no gato não especificam parâmetros de entrada: todos os parâmetros são implicitamente lidos da pilha.
- Foo chama a <função, que exibe o primeiro item na pilha, compara-a com 10 e empurra True ou False de volta à pilha.
- Em seguida, empurramos os valores 0 e 42 na pilha: os embrulhamos entre colchetes para garantir que eles sejam empurrados para a pilha sem avaliar. Isso ocorre porque eles serão usados como os branchs “then” e “else” (respectivamente) para a chamada para a função if na próxima linha.
- A função if exibe 3 itens fora da pilha: a condição booleana, o branch “then” e o branch “else”. Dependendo do valor da condição booleana, ele irá empurrar o resultado do branch “then” ou “else” de volta para a pilha.
- Finalmente, empurramos 20 para a pilha e chamamos a função foo.
- Quando tudo é dito e feito, acabaremos com o número 42.
Para uma introdução muito mais detalhada, veja The Joy of Concatenative Languages.
Este estilo de programação tem algumas propriedades interessantes: os programas podem ser divididos e concatenados de inúmeras maneiras de criar novos programas; Sintaxe notavelmente mínima (ainda mais mínima do que LISP) que leva a programas muito concisos; Forte suporte a Meta-Programação. Eu encontrei uma programação concatenativa para ser uma experiência de pensamento de abertura de olho, mas não fui vendido em sua praticidade. Parece que você tem que lembrar ou imaginar o estado atual da pilha em vez de poder lê-lo a partir dos nomes das variáveis no código, o que pode dificultar a argumentação sobre o código.
Programação declarativa
Exemplo de linguagens: Prolog, SQL
A programação declarativa existe há muitos anos, mas a maioria dos programadores ainda desconhece isso como um conceito. Aqui está a essência: na maioria dos idiomas convencionais, você descreve como resolver um problema particular; Em linguagens declarativas, você simplesmente descreve o resultado desejado, e a própria linguagem descobre como chegar lá.
Por exemplo, se você estiver escrevendo um algoritmo de classificação do zero em C, você pode escrever as instruções para o tipo de mesclagem, que descreve, passo a passo, como dividir recursivamente o conjunto de dados pela metade e combiná-lo novamente em ordem ordenada: Aqui está um exemplo. Se você estivesse classificando números em um idioma declarativo como o Prolog, em vez disso, descreveria a saída desejada: “Eu quero a mesma lista de valores, mas cada item no índice i deve ser menor ou igual ao item no índice i + 1 “. Compare a solução C anterior com este código Prolog:
sort_list(Input, Output) :-
permutation(Input, Output),
check_order(Output).
check_order([]).
check_order([Head]).
check_order([First, Second | Tail]) :-
First =< Second,
check_order([Second | Tail]).
Se você usou o SQL, você fez uma forma de programação declarativa e talvez não tenha percebido: quando você emite uma consulta, selecione X de Y, onde Z, você está descrevendo o conjunto de dados que deseja retornar; É o mecanismo de banco de dados que realmente descobre como executar a consulta. Você pode usar o comando explain na maioria dos bancos de dados para ver o plano de execução e descobrir o que aconteceu por trás do código em execução.
A beleza das linguagens declarativas é que elas permitem que você trabalhe em um nível de abstração muito maior: seu trabalho é apenas descrever a especificação para a saída que deseja. Por exemplo, o código para um simples sudoku solver em prolog apenas lista o que cada linha, coluna e diagonal de um quebra-cabeça sudoku resolvido deve ser semelhante:
sudoku(Puzzle, Solution) :-
Solution = Puzzle,
Puzzle = [S11, S12, S13, S14,
S21, S22, S23, S24,
S31, S32, S33, S34,
S41, S42, S43, S44],
fd_domain(Solution, 1, 4),
Row1 = [S11, S12, S13, S14],
Row2 = [S21, S22, S23, S24],
Row3 = [S31, S32, S33, S34],
Row4 = [S41, S42, S43, S44],
Col1 = [S11, S21, S31, S41],
Col2 = [S12, S22, S32, S42],
Col3 = [S13, S23, S33, S43],
Col4 = [S14, S24, S34, S44],
Square1 = [S11, S12, S21, S22],
Square2 = [S13, S14, S23, S24],
Square3 = [S31, S32, S41, S42],
Square4 = [S33, S34, S43, S44],
valid([Row1, Row2, Row3, Row4,
Col1, Col2, Col3, Col4,
Square1, Square2, Square3, Square4]).
valid([]).
valid([Head | Tail]) :- fd_all_different(Head), valid(Tail).
Veja como você executaria o solucionador sudoku acima:
| ?- sudoku([_, _, 2, 3,
_, _, _, _,
_, _, _, _,
3, 4, _, _],
Solution).
S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2]
A desvantagem, infelizmente, é que as linguagens de programação declarativas podem facilmente atingir os estrangulamentos do desempenho. O algoritmo de classificação ingênuo acima é provavelmente O (n!); O sudoku solver acima faz uma pesquisa de força bruta; E a maioria dos desenvolvedores teve que fornecer sugestões de banco de dados e índices extras para evitar planos caros e ineficientes ao executar consultas SQL.
Programação simbólica
Exemplo de linguagem: Aurora
A linguagem Aurora é um exemplo de programação simbólica: o “código” que você escreve nessas linguagens pode incluir não apenas texto simples, mas também imagens, equações matemáticas, gráficos, gráficos e muito mais. Isso permite que você manipule e descreva uma grande variedade de dados no formato nativo desses dados, em vez de descrevê-lo tudo no texto. A Aurora também é completamente interativa, mostrando os resultados de cada linha de código instantaneamente, como um REPL em esteroides.
A linguagem Aurora foi criada por Chris Granger, que também construiu a Light Table IDE. Chris descreve a motivação para Aurora em seu post Para uma melhor programação(Toward a better programming): alguns dos objetivos são tornar a programação mais observável, direta e reduzir a complexidade incidental. Para mais informações, certifique-se de ver as conversas incríveis de Bret Victor: Inventando sobre o Princípio, a mídia para pensar o impensável, e Programação Aprendizagem.
Atualização: “programação simbólica” provavelmente não é o termo certo para usar na Aurora. Veja o wiki de programação simbólica para mais informações.
Programação baseada no conhecimento
Exemplos: Wolfram Language
Tal como a linguagem Aurora mencionada acima, a Linguagem Wolfram também é baseada em programação simbólica. No entanto, a camada simbólica é meramente uma maneira de fornecer uma interface consistente ao núcleo da Linguagem Wolfram, que é uma programação baseada no conhecimento: incorporado ao idioma é uma vasta gama de bibliotecas, algoritmos e dados. Isso torna mais fácil fazer tudo, desde gráficos de suas conexões no Facebook, até manipulação de imagens, pesquisa de clima, processamento de consultas de linguagem natural, planejamento de direções em um mapa, resolução de equações matemáticas e muito mais.
Eu suspeito que o Wolfram Languages possui a maior “biblioteca padrão” e o conjunto de dados de qualquer idioma existente. Também estou entusiasmado com a idéia de que a conectividade com a Internet é uma parte inerente da escrita do código: é quase como um IDE onde a função de auto-preenchimento faz uma pesquisa do google. Vai ser muito interessante ver se o modelo de programação simbólica é tão flexível quanto o Wolfram reivindica e realmente pode tirar proveito de todos esses dados.
Atualização: embora Wolfram afirma que o Wolfram Language é compatível com “programação simbólica” e “programação de conhecimento”, esses termos possuem definições ligeiramente diferentes. Consulte os wikis do nível de conhecimento e da programação simbólica para obter mais informações.
Obs: Este artigo é uma livre tradução da postagem de Yevgeniy Brikman em seu Blog particular sobre tecnologia, segue link original abaixo.
Obrigado por enviar o seu comentário minha jóia!