Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 07: Funções de Ordem Superior e Paralelismo sem Suor (Nem `Mutex`, nem `Deadlocks`) |
| 2 | |
| 3 | Em quase todas as linguagens de programação há um momento dramático na vida de um projeto: o dia em que alguém diz numa reunião: |
| 4 | |
| 5 | > *"Isto está lento. É preciso paralelizar com threads."* |
| 6 | |
| 7 | Faz-se silêncio na sala. Toda a gente sabe o que vem a seguir: semanas a caçar *race conditions* fantasma que só acontecem às terças-feiras de lua cheia, bloqueios mútuos (*deadlocks*) e erros irreprodutíveis. |
| 8 | |
| 9 | Em Ridgun, quando alguém diz *"é preciso paralelizar"*, basta antepor `||` às suas operações de lista (**`||map`** e **`||filter`**) ou colocar os seus cálculos num bloco paralelo `||` com setas `->` e ir tomar um café. |
| 10 | |
| 11 | Porquê? Porque **quando as suas funções são puras e os seus dados são imutáveis por omissão, dois núcleos de CPU a trabalhar ao mesmo tempo jamais podem pisar os calcanhares um do outro**. |
| 12 | |
| 13 | Vamos ver isto passo a passo. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 7.1. Funções que recebem funções: `map`, `||map`, `filter`, `||filter` e `fold` |
| 18 | |
| 19 | Em Ridgun uma função é mais um valor. Pode passá-la por parâmetro tal como passa o número `7`. |
| 20 | |
| 21 | A linguagem traz de série os combinadores de coleções para transformar listas sem escrever uma única recursão manual, tanto em modo sequencial (`map`, `filter`, `fold`) como em modo paralelo multicore com o prefixo `||` (**`||map`**, **`||filter`**): |
| 22 | |
| 23 | ### 1. `map` (sequencial) e `||map` (paralelo multicore) |
| 24 | Suponha que tem uma função `pesado Int => Int` e uma lista com números: |
| 25 | |
| 26 | ```ridgun |
| 27 | pesado Int => Int |
| 28 | pesado x = (x * x) + (x * 3) |
| 29 | |
| 30 | main => () |
| 31 | main = |
| 32 | dados = [1, 2, 3, 4, 5, 6, 7, 8] |
| 33 | -- Sequencial estrito da esquerda para a direita: |
| 34 | r1 = map pesado dados |
| 35 | -- Paralelo multicore com o prefixo `||`: |
| 36 | r2 = ||map pesado dados |
| 37 | echo r1 |
| 38 | echo r2 |
| 39 | ``` |
| 40 | |
| 41 | - Com **`map`**, o Ridgun aplica a função elemento a elemento da esquerda para a direita na thread atual (ideal para listas pequenas ou funções com efeitos de entrada/saída ordenados). |
| 42 | - Com **`||map`**, o runtime do Ridgun distribui os elementos de `dados` pelas threads trabalhadoras do seu processador e reconstrói a lista final **exatamente pela mesma ordem**, garantindo que `||map f xs == map f xs`. |
| 43 | |
| 44 | ### 2. `filter` (sequencial) e `||filter` (paralelo): Ficar apenas com os que cumprem uma condição |
| 45 | Passa-lhe uma função que devolva `Bool` (um *predicado*) e uma lista: |
| 46 | |
| 47 | ```ridgun |
| 48 | e positivo Int => Bool |
| 49 | e positivo n = n > 0 |
| 50 | |
| 51 | main => () |
| 52 | main = |
| 53 | limpos = filter e positivo [-5, 10, 0, -2, 42] |
| 54 | limpos_par = ||filter e positivo [-5, 10, 0, -2, 42] |
| 55 | echo limpos -- Imprime [10, 42] |
| 56 | echo limpos_par -- Imprime [10, 42] |
| 57 | ``` |
| 58 | |
| 59 | ### 3. `fold`: Reduzir toda uma lista a um único resultado |
| 60 | Passa-lhe uma função combinadora de dois argumentos `(Acumulado Elemento => Acumulado)`, o valor inicial e a lista: |
| 61 | |
| 62 | ```ridgun |
| 63 | acumular produto Int Int => Int |
| 64 | acumular produto acc x = acc * x |
| 65 | |
| 66 | main => () |
| 67 | main = |
| 68 | echo (fold acumular produto 1 [2, 3, 4, 5]) -- 1 * 2 * 3 * 4 * 5 = 120 |
| 69 | ``` |
| 70 | |
| 71 | ### Como escrevo eu a minha própria função que receba outra função? |
| 72 | Muito fácil: na assinatura de tipos coloca a assinatura da função convidada entre parênteses `(Entrada => Saida)`: |
| 73 | |
| 74 | ```ridgun |
| 75 | executar duas vezes (A => A) A => A |
| 76 | executar duas vezes f x = f (f x) |
| 77 | ``` |
| 78 | |
| 79 | ### 4. Tubagens fluidas da esquerda para a direita (`->`) — O estilo 100% Ridgun! |
| 80 | Quando encadeia várias transformações (`map`, `filter`, `fold` ou as suas próprias funções e operadores `op ::`), aninhar parênteses da direita para a esquerda (`compact l (filter g (map f xs))`) obriga-o a ler de dentro para fora. |
| 81 | |
| 82 | No Ridgun, o **estilo idiomático recomendado (`2b`)** lê-se da esquerda para a direita com o operador de tubagem **`->`**, onde **o que vem da esquerda entra sempre como primeiro argumento** da função ou `op ::` seguinte: |
| 83 | |
| 84 | ```ridgun |
| 85 | -- Estilo idiomático recomendado no Ridgun (2b): |
| 86 | xs map (x => x * 2) -> filter (x => x > 10) -> fold (acc x => acc + x) 0 -> resultado |
| 87 | |
| 88 | -- E se uma função devolver uma tupla, filtra-a com uma máscara e desempacota-se sozinha: |
| 89 | analisar pacote "v0.9.9" -> (+ _ _ +) -> combinar codigo -> resumo |
| 90 | ``` |
| 91 | |
| 92 | Além disso, qualquer expressão com efeitos secundários (escrever um ficheiro, invocar o sistema) pode ficar sozinha na sua linha sem `echo` nem variáveis mudas `_ =`, ou terminar em `-> ()` se quiser descartar explicitamente o seu retorno. |
| 93 | |
| 94 | ### 5. Fechos léxicos e funções anónimas (`(x => expr)`, `(acc x => expr)`) |
| 95 | Quando quiser passar uma função curta em linha ou capturar variáveis locais do âmbito atual (como um deslocamento, um prefixo ou uma sessão ativa) sem declarar uma função global auxiliar, usa um **fecho léxico** entre parênteses com **`=>`**: |
| 96 | |
| 97 | ```ridgun |
| 98 | fn :: processar com sessao => [Int] |
| 99 | processar com sessao = |
| 100 | base = 10 |
| 101 | bonus = 3 |
| 102 | somados = map (x => x + base) [1, 2, 3] |
| 103 | filtrados = filter (x => x >= bonus) [1, 2, 3, 4] |
| 104 | total = fold (acc x => acc + x + bonus) 0 [1, 2, 3] |
| 105 | pares = map ((a b) => a + b + base) [(1, 2), (3, 4)] |
| 106 | somados :: filtrados :: [total] :: pares |
| 107 | ``` |
| 108 | |
| 109 | Cinco regras mantêm as lambdas limpas e sem ambiguidade: |
| 110 | - **Parênteses obrigatórios**: toda a lambda vai sempre entre parênteses `(... => ...)`. |
| 111 | - **Lambda sem parâmetros (`(=> expr)`)**: tal como `fn :: main => ()` não leva `()` antes de `=>`, uma lambda sem parâmetros escreve-se `(=> expr)` (e o seu tipo é `(=> Tipo)`), nunca `(() => expr)`. |
| 112 | - **Vários parâmetros face a tupla (`(a b => ...)` vs. `((a b) => ...)`)**: `(acc x => ...)` recebe dois parâmetros separados; `((a b) => ...)` recebe uma única tupla e desestrutura-a. |
| 113 | - **Corpo de várias linhas**: se colocar uma quebra de linha após `=>`, pode escrever um bloco indentado de vários passos antes do `)` de fecho: |
| 114 | ```ridgun |
| 115 | dobros = map (x => |
| 116 | passo = x * 2 |
| 117 | passo + base |
| 118 | ) [1, 2, 3] |
| 119 | ``` |
| 120 | - **Maiúsculas face a minúsculas**: `(Conn => Conn)` é um tipo de função (inicial maiúscula) e `(conn => conn)` é um valor lambda (inicial minúscula). |
| 121 | |
| 122 | As variáveis capturadas (`base`, `bonus`) são empacotadas automaticamente com gestão determinística de memória (`RMM1`/`RMM2`), sem fugas e sem pausas de coletor de lixo. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## 7.2. A joia da coroa: Blocos `||` com `->` como Grafos Paralelos (DAGs) |
| 127 | |
| 128 | É aqui que o Ridgun muda a sua forma de pensar. |
| 129 | |
| 130 | Numa linguagem imperativa normal, quando escreve três linhas seguidas: |
| 131 | 1. Primeiro executa-se a linha 1. |
| 132 | 2. Depois a linha 2 (mesmo que não tenha nada a ver com a linha 1). |
| 133 | 3. Depois a linha 3. |
| 134 | |
| 135 | Isso é obrigar o computador a trabalhar com antolhos. Em Ridgun, quando usa barras paralelas **`||`** com setas **`->`** (sem necessidade de escrever nenhuma palavra-chave como `where`), não está a dar ordens sequenciais: está a desenhar um **Grafo Acíclico Dirigido (DAG)** que diz que cálculo alimenta que variável: |
| 136 | |
| 137 | ```ridgun |
| 138 | fib Int => Int |
| 139 | fib 0 = 0 |
| 140 | fib 1 = 1 |
| 141 | fib n = (fib (n - 1)) + (fib (n - 2)) |
| 142 | |
| 143 | fact Int => Int |
| 144 | fact 0 = 1 |
| 145 | fact n = n * (fact (n - 1)) |
| 146 | |
| 147 | super calculo => Int |
| 148 | super calculo = ramo_esquerdo + ramo_direito |
| 149 | || fib 10 -> ramo_esquerdo |
| 150 | || fact 6 -> ramo_direito |
| 151 | ``` |
| 152 | |
| 153 | Leia em voz alta: |
| 154 | - *"Quero calcular `ramo_esquerdo + ramo_direito`, enquanto em paralelo (`||`) `fib 10` vai para `ramo_esquerdo` (`->`) e `fact 6` vai para `ramo_direito` (`->`)."* |
| 155 | |
| 156 | ### O que faz o `ridgunc` nos bastidores com as suas linhas `||`: |
| 157 | 1. Olha para as setas `->` e monta a árvore de dependências (DAG). |
| 158 | 2. **Caça ciclos estaticamente**: se por engano escrever `|| a + 1 -> b` e `|| b + 1 -> a`, o compilador diz-lhe na hora *"Alto lá! Há uma dependência circular"* e não o deixa compilar um bloqueio infinito. |
| 159 | 3. Vê que `fib 10` e `fact 6` não dependem um do outro, pelo que têm luz verde para correr em paralelo antes de se somarem em `ramo_esquerdo + ramo_direito`. |
| 160 | |
| 161 | Pode inclusive montar um pipeline de vários andares dentro do mesmo bloco `||` — seja no rodapé da expressão ou logo depois do `=` — e o compilador deduzirá sozinho que andares podem avançar ao mesmo tempo e quais têm de esperar: |
| 162 | |
| 163 | ```ridgun |
| 164 | balanco trimestral Int => Int |
| 165 | balanco trimestral base = |
| 166 | || base * 100 -> vendas_brutas |
| 167 | || base * 40 -> custos_fixos |
| 168 | || vendas_brutas - custos_fixos -> lucro_bruto ||. |
| 169 | lucro_bruto * 80 / 100 |
| 170 | ``` |
| 171 | |
| 172 | Repare no detalhe **`||.`** no final da última linha `||`: tal como `|.` fecha uma árvore de guardas aninhada, **`||.`** fecha o bloco paralelo `||` quando por baixo continuam instruções sequenciais normais. Aqui `vendas_brutas` e `custos_fixos` arrancam em paralelo; quando ambos estão prontos calcula-se `lucro_bruto`, e finalmente devolve-se `lucro_bruto * 80 / 100`. Tudo sem um único `async`, `Future`, `Promise` nem `Mutex`. |
| 173 | |
| 174 | --- |
| 175 | |
| 176 | ## 7.3. O atalho `||` sem nomes: Condutas paralelas que devolvem uma tupla |
| 177 | |
| 178 | E se só quiser lançar dois ou três cálculos em paralelo e recolhê-los juntos numa tupla sem sequer inventar nomes de variáveis com `->`? |
| 179 | |
| 180 | Usa `||` diretamente: |
| 181 | |
| 182 | ```ridgun |
| 183 | par paralelo Int => (Int, Int) |
| 184 | par paralelo n = |
| 185 | || fib n |
| 186 | || fact n |
| 187 | ``` |
| 188 | |
| 189 | Direto, visual e devolve a tupla `(fib n, fact n)`. |
| 190 | |
| 191 | --- |
| 192 | |
| 193 | ## 7.4. Quando quer o volante manual: o Modelo Nativo de Agentes (`agent`, `send ... to`, `receive from`) e Canais CSP |
| 194 | |
| 195 | O paralelismo automático de blocos `||` e `||map` cobre 90% dos casos. Mas quando está a programar um servidor persistente, sessões de utilizador com estado isolado, uma fila de trabalhos ou um pipeline produtor-consumidor, quer **agentes concorrentes com memória isolada e passagem de mensagens tipada**. |
| 196 | |
| 197 | No Ridgun não é preciso inventar uma sintaxe estranha como `actor ::`: **qualquer função normal (`fn ::` ou `op ::`) transforma-se num agente concorrente numa única linha com `agent`**: |
| 198 | |
| 199 | ### 1. Agentes sem estado e com estado isolado (`agent`, `send ... to`, `receive from`, `stop`) |
| 200 | - `ag = agent (minha funcao)`: transforma uma função `M => R` num agente sem estado do tipo `Agent M R`. |
| 201 | - `ag = agent (funcao com estado) estado_inicial`: transforma uma função `S M => S` (ou `S M => S R`) num agente com estado isolado que recorda o seu estado mensagem após mensagem sem um único `Mutex`. |
| 202 | - `send msg to ag` (ou com vários argumentos: `send a b to ag`): envia uma mensagem tipada para a caixa de entrada do agente. |
| 203 | - `res = receive from ag` (ou `try receive from ag` sem bloquear): recolhe a resposta seguinte da caixa de saída do agente. |
| 204 | - `stop ag`: termina o agente e liberta o seu estado automaticamente através de `RMM1`/`RMM2`. |
| 205 | |
| 206 | ```ridgun |
| 207 | fn :: somar visita Int Int => Int |
| 208 | somar visita acc n = acc + n |
| 209 | |
| 210 | fn :: processar lote Int Int => Int |
| 211 | processar lote id t = (id * 100) + t |
| 212 | |
| 213 | fn :: main => () |
| 214 | main = |
| 215 | contador = agent (somar visita) 10 |
| 216 | send 5 to contador |
| 217 | send 7 to contador |
| 218 | v1 = receive from contador |
| 219 | v2 = receive from contador |
| 220 | stop contador |
| 221 | |
| 222 | trabalhador = agent (processar lote) |
| 223 | send 3 25 to trabalhador |
| 224 | lote = receive from trabalhador |
| 225 | stop trabalhador |
| 226 | echo "Visitas:" v1 "->" v2 "| Lote:" lote |
| 227 | ``` |
| 228 | |
| 229 | ### 2. Escolha inteligente em tempo de compilação (*Green Threads* vs. Threads Reais do Kernel) |
| 230 | Uma pergunta habitual é: *"Os blocos `||`, `||map` e os agentes `agent` criam sempre threads do sistema operativo ou correm como tarefas leves?"* |
| 231 | |
| 232 | Em Ridgun, **o compilador toma essa decisão por si analisando estaticamente cada função**: |
| 233 | - **Funções leves (`TierTrivial` / `StratInlineSequential`)**: Se a função que passa a `||`, `||map`, `||filter` ou `agent` tiver pouca profundidade (aritmética, formatação de cadeias, transição rápida de estado de uma sessão HTTP), aquecer threads do sistema operativo custaria mais do que o próprio cálculo. O compilador executa-a como um passo *green-thread* ultraleve sem sobrecarga de `SYS_clone`. |
| 234 | - **Cálculos pesados ou recursivos profundos (`TierDynamicRecursive` / `TierHeavyCPU`)**: Se a função tiver recursão profunda (como `fib` ou `fact`) ou álgebra de tensores (`einsum`, `hadamard`, `@`), o compilador gera automaticamente o despacho multinúcleo sobre **threads reais do kernel (`SYS_clone`)**, processando os ramos `||` ou a fila de mensagens do `agent` em paralelo real sobre todos os núcleos do seu CPU. |
| 235 | |
| 236 | ### 3. Passar mensagens por canais limitados (`new bounded channel`) |
| 237 | Tal como em Go ou Erlang, além de agentes também pode criar tubos explícitos para ligar etapas: |
| 238 | |
| 239 | - `new bounded channel capacidade`: cria um canal com espaço para `capacidade` mensagens. |
| 240 | - `send through valor canal` (ou `chan send valor canal`): coloca uma mensagem no tubo. |
| 241 | - `receive from canal` (ou `chan recv canal`): retira a mensagem seguinte do tubo. |
| 242 | - `try receive from canal`: verifica se há mensagem sem ficar à espera; devolve `Some valor` ou `None`. |
| 243 | - `receive from timeout canal ticks`: aguarda um limite de tempo e devolve `Ok valor` ou `Err Timeout`. |
| 244 | |
| 245 | ```ridgun |
| 246 | fn :: main => () |
| 247 | main = |
| 248 | tubo = new bounded channel 4 |
| 249 | _ = send through "Tarefa A concluída" tubo |
| 250 | _ = send through "Tarefa B concluída" tubo |
| 251 | primeiro = receive from tubo |
| 252 | segundo = receive from tubo |
| 253 | echo primeiro "|" segundo |
| 254 | ``` |
| 255 | |
| 256 | ### 4. Agentes sumidouro (*fire-and-forget* `agent sink`), caixas de correio globais (`agent ::`) e testes determinísticos (`with agent`) |
| 257 | Quando precisa de auditoria assíncrona, métricas ou rastreabilidade transversal sem poluir todas as assinaturas nem acumular respostas por ler numa fila de saída, o Ridgun oferece **agentes sumidouro (`agent sink`)** e **caixas de correio globais de topo (`agent ::`)** (`agent dropped`, `agent flush`, `agent state` e `with agent audit_log = espiao do ...`), além de descritores `Channel` e `Agent` partilháveis entre múltiplos trabalhadores. |
| 258 | |
| 259 | E agora, aperte o cinto de segurança para o **Capítulo 08**, o grande final: **tensores com dimensões verificadas na compilação, variáveis de âmbito (`context`/`shared`), regiões de memória determinísticas, acesso direto a hardware MMIO e programação de sistemas UNIX/POSIX**. |