Skip to content
Ridgun
English

Ridgun

The language itself: a compiler that compiles itself, the stable project tool, the specification and the book

07-ordem-superior-e-paralelismo-dag.md259 lines · 13.8 KBRaw
1# Capítulo 07: Funções de Ordem Superior e Paralelismo sem Suor (Nem `Mutex`, nem `Deadlocks`)
2
3Em 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
7Faz-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
9Em 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
11Porquê? 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
13Vamos ver isto passo a passo.
14
15---
16
17## 7.1. Funções que recebem funções: `map`, `||map`, `filter`, `||filter` e `fold`
18
19Em Ridgun uma função é mais um valor. Pode passá-la por parâmetro tal como passa o número `7`.
20
21A 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)
24Suponha que tem uma função `pesado Int => Int` e uma lista com números:
25
26```ridgun
27pesado Int => Int
28pesado x = (x * x) + (x * 3)
29
30main => ()
31main =
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
45Passa-lhe uma função que devolva `Bool` (um *predicado*) e uma lista:
46
47```ridgun
48e positivo Int => Bool
49e positivo n = n > 0
50
51main => ()
52main =
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
60Passa-lhe uma função combinadora de dois argumentos `(Acumulado Elemento => Acumulado)`, o valor inicial e a lista:
61
62```ridgun
63acumular produto Int Int => Int
64acumular produto acc x = acc * x
65
66main => ()
67main =
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?
72Muito fácil: na assinatura de tipos coloca a assinatura da função convidada entre parênteses `(Entrada => Saida)`:
73
74```ridgun
75executar duas vezes (A => A) A => A
76executar duas vezes f x = f (f x)
77```
78
79### 4. Tubagens fluidas da esquerda para a direita (`->`) — O estilo 100% Ridgun!
80Quando 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
82No 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):
86xs 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:
89analisar pacote "v0.9.9" -> (+ _ _ +) -> combinar codigo -> resumo
90```
91
92Alé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)`)
95Quando 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
98fn :: 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
109Cinco 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
122As 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
130Numa linguagem imperativa normal, quando escreve três linhas seguidas:
1311. Primeiro executa-se a linha 1.
1322. Depois a linha 2 (mesmo que não tenha nada a ver com a linha 1).
1333. Depois a linha 3.
134
135Isso é 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
138fib Int => Int
139fib 0 = 0
140fib 1 = 1
141fib n = (fib (n - 1)) + (fib (n - 2))
142
143fact Int => Int
144fact 0 = 1
145fact n = n * (fact (n - 1))
146
147super calculo => Int
148super calculo = ramo_esquerdo + ramo_direito
149 || fib 10 -> ramo_esquerdo
150 || fact 6 -> ramo_direito
151```
152
153Leia 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 `||`:
1571. Olha para as setas `->` e monta a árvore de dependências (DAG).
1582. **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.
1593. 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
161Pode 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
164balanco trimestral Int => Int
165balanco 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
172Repare 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
178E 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
180Usa `||` diretamente:
181
182```ridgun
183par paralelo Int => (Int, Int)
184par paralelo n =
185 || fib n
186 || fact n
187```
188
189Direto, 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
195O 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
197No 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
207fn :: somar visita Int Int => Int
208 somar visita acc n = acc + n
209
210fn :: processar lote Int Int => Int
211 processar lote id t = (id * 100) + t
212
213fn :: 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)
230Uma pergunta habitual é: *"Os blocos `||`, `||map` e os agentes `agent` criam sempre threads do sistema operativo ou correm como tarefas leves?"*
231
232Em 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`)
237Tal 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
246fn :: 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`)
257Quando 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
259E 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**.