1 pontos por GN⁺ 2024-11-14 | 1 comentários | Compartilhar no WhatsApp
  • Raku se parece mais com um laboratório de recursos de linguagem, onde várias ideias são testadas diretamente, do que com um projeto único e altamente refinado; por isso, dá para ver em um só lugar experimentos que outras linguagens talvez venham a adotar
  • Junctions propagam operações sobre um conjunto de valores possíveis e depois as reduzem com quantificadores booleanos como all, any e one, permitindo expressar condições complexas de forma curta
  • O * de Whatevers é usado em intervalos infinitos, funções anônimas e indexação baseada no tamanho da lista, criando um estilo de código conciso, mas incomum
  • As regexes de Raku abandonam a compatibilidade com PCRE em troca de gramáticas componíveis e recursos práticos como separadores de valores, lookahead mais legível e ignorar espaços em branco por padrão
  • Considerando Hyperoperators, Pair Syntax, Slangs, RakuAST e até o Rakudo Star, Raku é uma linguagem que experimenta amplamente, da gramática à forma de distribuição

Por que Raku parece um laboratório de recursos de linguagem

  • Depois de usar Raku de fato por mais de um ano, a linguagem parece mais próxima de um “laboratório para recursos de linguagem
  • Os cinco modelos de concorrência e as várias maneiras de fazer uma mesma coisa revelam um caráter experimental, voltado a “ver o que acontece”
  • O motivo de muitos recursos interagirem de forma estranha e terem muitos casos de borda também é que executar 100 experimentos é muito mais fácil do que refiná-los para funcionarem bem em conjunto
  • Os recursos tratados aqui são experimentos ao estilo de Raku que talvez um dia apareçam em outras linguagens de programação

Junctions: sobreposição de valores possíveis e quantificadores booleanos

  • Junctions representam uma sobreposição de valores possíveis, e quando uma operação é aplicada a uma junction, a mesma operação é aplicada a todos os valores internos
    • 2|10 vira any(2, 10)
    • 2&10 + 3 vira all(5, 13)
    • (1&2) + (10^20) é combinado como all(one(11, 21), one(12, 22))
  • O principal uso de Junctions é expressar fórmulas booleanas como all e any
    • não há como desmontar estruturalmente uma junction
    • para usá-la na prática, primeiro é preciso reduzi-la a um booleano
  • so força a conversão de uma junction para booleano
    • so (1&2) + (10^20) < 15 é True
    • so (1&2) + (10^20) > 0 é False
  • Em Raku, list.all não é um método que recebe argumentos, mas sim um método sem argumentos que transforma a lista em junction
    • <1 2 3>.all vira all(1, 2, 3)
    • ao aplicar is-prime($x), isso é aplicado a cada valor, resultando em algo como all(False, True, True)
  • Compondo Junctions, também dá para criar comportamentos como unique
    • “todo elemento é igual a exatamente um elemento” pode ser expresso como {.all == .one}
    • <1 2 3 7> é verdadeiro, e <1 2 3 7 2> é falso

Whatevers: intervalos infinitos e funções anônimas com *

  • O * de Whatevers é um símbolo whatever que assume vários papéis em Raku
  • Em intervalos ou sequências, * significa que não há limite
    • 1..* é 1..Inf
    • (2,4,8...*)[17] é 262144
  • Expressões que contêm * são promovidas a funções anônimas
    • esse comportamento é chamado de whatever-priming
    • o resultado é WhateverCode e, tirando o tipo, não se distingue de outras funções
    • (* + 10)(2) é 12
    • (^10).map(* % 2) cria o padrão 0 1 0 1 ...
  • Se houver dois * na expressão, cada um vira um argumento posicional separado
    • (2, 30, 4, 50).map(* + *) retorna (32, 54)
    • isso é útil em definições complicadas de Fibonacci, mas não é certo que seja melhor do que um comportamento em que cada * represente o mesmo valor
  • Muitos métodos de Raku podem receber funções, então o priming é amplamente usado
    • l[*-1] parece um índice negativo, mas na prática [] passa o tamanho da lista para a função e consulta o índice calculado
    • se a lista tem tamanho 10, l[*-1] vira l[10-1], ou seja, o último elemento
    • l.head(2) são os dois primeiros elementos, e l.head(*-2) significa tudo, exceto os dois últimos
  • Passando outras funções para [], também dá para implementar estruturas como buffer circular de forma simples
    • @x[95 % *]-- calcula a posição usando o tamanho da lista

Regexes: gramáticas componíveis em vez de compatibilidade com PCRE

  • Raku usa uma nova sintaxe de regex, diferente de regex POSIX e de PCRE
    • regex POSIX têm uma estrutura cheia de barras invertidas e pontuação
    • PCRE é retrocompatível com POSIX e herdou essa estrutura ainda mais complexa de barras invertidas e pontuação
    • Perl 6 rompeu a retrocompatibilidade e introduziu uma nova sintaxe de regex
  • A melhoria mais marcante é a componibilidade
    • em muitas linguagens, regexes são combinadas concatenando strings de regex
    • em Raku, é possível inserir outra regex, como em /< foo >+/, para casar a regex foo uma ou mais vezes sem que ela vaze para a regex superior
  • Isso permite dividir regexes complexas em partes menores e mais legíveis
  • Ao quebrar a retrocompatibilidade, vários recursos de conveniência também foram adicionados
    • \d+ % ',' casa 1, 1,2 e 1,1,4, mas não casa 1, nem 12
    • lookahead e grupo non-capturing não usam combinações enigmáticas de símbolos
    • r1 && r2 casa apenas strings que satisfazem tanto r1 quanto r2
    • : pode interromper o backtracking
    • espaços em branco são ignorados por padrão e precisam ser ativados explicitamente no padrão de match
  • Independentemente da capacidade real de processamento, a notação de regex de Raku pode ser vista como um conjunto de recursos que também pode aparecer em outras linguagens

Hyperoperators: mapeamento generalizado que desce até listas aninhadas

  • A forma básica de Hyperoperators, l>>.method, é parecida com map, mas desce recursivamente até as sublistas
    • [1, [2, 3], 4]>>.succ vira [2 [3 4] 5]
  • Em Raku, uma chamada de função no formato f(list, *args) pode ser convertida para a forma de método list.&f(* args)
    • por isso, >>. funciona como um operador de mapeamento generalizado
    • junto com Whatever, pode ser usado como [1, [2, 3], 4]>>.&(*+1)
  • O hyperoperator binário l1 << op >> l2 aplica op elemento por elemento às duas listas
    • a lista menor se repete até a maior terminar
    • [1, 2, 3, 4, 5] <<+>> [10, 20] resulta em [11 22 13 24 15]
    • as formas << op<< ou >>op>> repetem apenas até a lista da esquerda ou da direita terminar
  • Também funciona com valor único e com listas aninhadas
    • [1, 2, 3, 4, 5] <<+>> 10 vira [11 12 13 14 15]
    • [1, [2, 3], 4, 5] <<+>> [10, 20] vira [11 [22 23] 14 25]
  • Hyperoperators entre dois hashes também têm um comportamento separado, aplicando op à união, interseção ou diferença entre hashes
  • O nome Hyperoperator vem do fato de todas as operações serem avaliadas em paralelo
    • até loops explícitos podem ser paralelizados com o prefixo hyper

Pair Syntax: unidade básica do hash e passagem concisa de flags

  • Pair é a unidade básica que compõe os hashes em Raku
    • um hash é uma coleção de pares key => value
    • pair é o tipo básico, e hash é uma coleção de pairs
  • Raku tem várias formas de sintaxe com dois-pontos para escrever pair de forma curta
    • my $x = 3; :$x é x => 3
    • :a<$x> é a => "$x"
    • :a($x) é a => 3
    • :3a é a => 3
  • O atalho mais importante é :key e :!key
    • eles correspondem a key => True e key => False, respectivamente
    • são usados como uma forma concisa de passar flags a métodos
  • O método match recebe vários colon pairs como flags
    • usa formas como :continue(:$c), :pos(:$p), :global(:$g), :overlap(:$ov)
    • na assinatura, :f(:$foo) define o parâmetro $foo e também cria o alias :f
    • quem chama pode definir a mesma flag com :f ou :foo
  • Argumentos colon pair têm ordem e posição flexíveis
    • "abab".match(/../, :g) faz match global
    • "abab".match(/../, :g, :ov) faz match com sobreposição
    • também é possível inverter a ordem e agrupar, como em "abab".match(:g:ov, /../)
    • argumentos nomeados podem até vir depois da chamada, como em "abab".match(:g, /../):ov:2nd
  • Essa sintaxe permite definir opções de método de forma bem mais curta do que match(global=True, overlap=True)

Slangs, RakuAST, Rakudo Star

  • Slangs são um recurso para alterar a gramática de Raku
  • RakuAST é um recurso que faz parsing de expressões Raku para AST dentro do próprio Raku
    • Q/my $x; $x++/.AST mostra uma declaração de variável e um incremento postfix na forma de RakuAST::StatementList
    • há também um exemplo que gera código Raku em uma notação parecida com outra linguagem, como Q/my $x; put $x/.AST.DEPARSE("NL")
  • Raku tem uma distribuição instalável chamada Rakudo Star
    • essa distribuição fornece alguns módulos de terceiros já pré-instalados
    • ela é próxima de um meio-termo entre o custo de manter uma grande biblioteca padrão e o custo de fazer o usuário procurar sozinho os pacotes adequados no ecossistema

1 comentários

 
GN⁺ 2024-11-14
Opiniões no Hacker News
  • Há algum tempo implementei de forma independente, para JavaScript, algo parecido com o recurso de regexes composicionais mencionado aqui, e mesmo anos depois ela ainda é uma das bibliotecas que mais levo para outros projetos.
    Ela deixa a sensação um pouco mais próxima de ter parsers de primeira classe dentro da linguagem, e há um exemplo usando isso para implementar o parsing de tipos de mídia com regexes: https://runkit.com/tolmasky/media-type-parsing-with-template...
    No npm: templated-regular-expression; GitHub: https://github.com/tolmasky/templated-regular-expression
    Claro que linguagens de programação deveriam ter parsers de verdade, e regexes não deveriam ser usadas como parsers, mas, se você vai usar regex de qualquer forma, poder dividi-la em pequenos pedaços é realmente ótimo.

    • Um “parser de verdade” não é poderoso o suficiente para fazer parse de Raku.
      As regexes de Raku são muito mais poderosas quando combinadas com gramáticas e, quando bem escritas, são mais fáceis de entender do que qualquer “parser de verdade”. Para fazer parse de Raku com um parser comum, seria preciso conseguir adicionar e remover itens do parser durante o parsing; o parser de Raku lida com isso subclassificando a gramática atual para adicionar e remover itens dentro do escopo léxico atual e, quando o escopo termina, voltando à gramática anterior.
      Em Raku, regexes são outra gramática para escrever código; elas só têm uma gramática e um comportamento padrão um pouco diferentes, e podem ter parâmetros e variáveis. Se a gramática de regex não for adequada para uma tarefa, dá para inserir gramática Raku normal, fazer o que for necessário e depois voltar para a gramática de regex.
      Por exemplo, é possível casar uma ou mais ocorrências de A seguidas pelo mesmo número de B e pelo mesmo número de C; na maioria das gramáticas de regex, para casar o mesmo número de A e B, você precisaria de recursão no meio, mas assim fica difícil tratar também C do mesmo modo, e a leitura fica muito mais complicada. A abordagem do Raku não precisa de backtracking nem recursão, então pode rodar bem rápido.
      Se você agrupar isso em uma gramática, obtém uma árvore de parse completa; também é possível fazer sem gramática, mas usar gramática é mais fácil. Como exemplo de parser real, costumo recomendar JSON::TINY::Grammar: https://github.com/moritz/json/blob/master/lib/JSON/Tiny/Gra...
      Para ser sincero, boa parte do design dos “parsers de verdade” parece um subproduto da RAM limitada dos computadores antigos. A etapa separada de tokenização servia para reduzir a RAM ocupada pelo código-fonte e deixar memória para a análise semântica e a compilação posteriores, e não acho que ela simplifique tanto assim essas etapas seguintes. O módulo JSON::Tiny acima cria estruturas de dados nativas de Raku por meio de uma classe de ações enquanto a gramática faz o parsing, então é como se fizesse parsing e compilação ao mesmo tempo.
  • Uso Raku em produção. Para processamento de texto, é a melhor linguagem, e a experiência de criar parsers é excelente.
    É surpreendente que ela não seja a linguagem de ponta para criar pipelines de texto com LLMs.

    • Cheguei tarde à conversa, mas queria saber se você conhece algum bom exemplo de como chamar uma API HTTPS em Raku e fazer polling até que ela retorne um valor específico.
    • Queria saber se você está usando algum pacote de LLM em Raku e, se estiver, qual.
  • Uau. Dá vontade de sair da indústria antes que chegue o dia em que eu tenha que manter uma base de código em Raku.

    • Curiosamente, depois de ler aquele post no blog, fiquei com vontade de largar o emprego e procurar o time do Raku para trabalhar lá. Talvez eu ainda seja ingênuo demais :)
    • Se você nunca viu código Raku comum, é compreensível ter essa reação ao ler o texto.
      Ao olhar livros introdutórios de Raku, muitas partes parecem Python com sintaxe da família C. É uma sintaxe centrada em chaves, mas a facilidade de uso, as estruturas de dados embutidas e os recursos de orientação a objetos são todos de alto nível.
      Se você conhece outras linguagens de script de alto nível, acho que o código Raku de scripts semelhantes seria bem fácil de ler. Não acho que a maioria das pessoas use, no código do dia a dia, os recursos realmente incomuns, e o Raku é mais flexível, mas em geral o código não parece obscuro.
    • É parecido com Perl. Ninguém quer manter, mas é muito divertido de usar. A expressividade é enorme.
      Isso aparece em recursos do Raku como a possibilidade de definir argumentos nomeados em forma abreviada, por exemplo :global(:$g), ou de chamar um match global de regex como match(/foo/, :g), assumindo um valor True. Perl também tem muitas coisas assim, todas para escrever de forma mais rápida e divertida, mas são menos legíveis para iniciantes.
    • É estranho que as pessoas digam coisas parecidas sobre a manutenção de bases de código escritas com auxílio de IA.
      Acho que isso vai virar uma diferença geracional. Uma geração inteira de programadores mais velhos pode acabar se sentindo deslocada em um ambiente de trabalho que, para a geração atual, é normal.
    • Não acho que Raku seja uma linguagem pensada para a “indústria”.
  • Alguns recursos são meio familiares. Hyper soa como uma versão mais improvisada de algo da área de recursion-schemes, e o * mostrado aqui é um pouco parecido com o _ do Scala.
    Gosto de usar _ em lambdas, então queria que todas as linguagens adotassem algo parecido.

  • > (2, 30, 4, 50).map(* + *) returns (32, 45) não deveria ser returns (32, 54)? O segundo termo seria 4+50.
    Talvez seja resultado de uma tradução palavra por palavra vinda do modo como alguns países dizem algo como “quatro e cinquenta”, em vez de “cinquenta e quatro” em inglês.

    • Verifiquei no rakudo e, na verdade, ele retorna (32 54). Parece que o autor apenas cometeu um deslize.
  • Então agora Perl é uma droga de entrada para linguagens da família APL?

    • Sim. Além disso, o texto nem abordou os metaoperadores
      Por exemplo, dá para usar o metaoperador de redução [ ] junto com o + infixo para “somar tudo”: [+] 1, 2, 3
  • Raku ainda tem velocidade como um grande problema. Fazer parsing de arquivos de log com expressões regulares é um ponto forte do Perl, mas mesmo o Raku mais recente, excluindo o tempo de inicialização, leva 6,5 vezes mais tempo que o Python 3.13

    • Seria preciso apresentar um exemplo junto. Pela minha experiência, há casos em que Raku é mais rápido e casos em que é mais lento; então dizer apenas que “Raku leva 6,5 vezes mais tempo que o Python 3.13”, sem ver em que ele é lento, não significa muita coisa
  • Foi realmente surpreendente que (2,4,8...*)[17] resulte em 262144

    • Na minha cabeça, a palavra que surgiu imediatamente foi “horrível”, seguida de “resultado horrível” e do problema da parada, mas cada um tem seu gosto
    • A detecção de incremento já existia há muito tempo no Perl 6, então não é um recurso novo
      Excluindo o Whatever, a novidade após Perl 6/Raku provavelmente está mais no lado da avaliação preguiçosa
    • É bastante consistente
      (1,3,9...*)[4,5] vira (81 243), e (1,3,9...*)[(1..3)] vira (3 9 27)
      Também dá para aninhar, permitindo expressões como (1,2,4...*)[(1,2,4...*)[1,2,3]]
  • O texto acabou selecionando e abordando alguns recursos interessantes do Raku
    Acho que os recursos mais importantes do Raku são a interface de linha de comando e as gramáticas (grammar)
    Suporte a interface de linha de comando é um recurso comum; por exemplo, há implementações de docopt e casos de adoção. Mas em Raku esse recurso é embutido e fácil de usar
    Já gramáticas são um recurso raro no sentido de que a linguagem de programação oferece gramáticas como componentes de primeira classe, permitindo criar e combinar gramáticas com programação orientada a objetos

  • Acompanhei este projeto por alguns anos e ele é interessante, mas é realmente uma pena que o Perl 6 pareça ter se perdido demais nessa viagem pelo país do espelho

    • Fico curioso para saber como e com o que Patrick Michaud anda trabalhando hoje