monads

Um pequeno kit funcional: Maybe, Either e uma lista encadeada, com os maps, binds, operadores e travessias para combiná-los.

import monads

O monads é escrito na própria Bern - toda função abaixo é uma função Bern comum que você pode ler em lib/monads.brn. Ele é construído sobre três tipos algébricos, que (desde a Bern 2.2) comparam com == e podem ser misturados dentro de uma lista.

Os tipos

adt Maybe  = Just Any | Nothing
adt Either = Left Any | Right Any
adt List   = Nil | Cons Any List

Maybe modela um valor que pode estar ausente - Just(x) ou Nothing() - então "sem resultado" é explícito em vez de um valor sentinela como -1 ou null. Either modela um de dois desfechos, por convenção Left(e) para uma falha que carrega informação e Right(x) para um sucesso. List é uma lista encadeada clássica para código que quer fazer pattern matching da estrutura diretamente; list_from / list_to fazem a ponte para as listas embutidas da Bern.

Construtores sem argumentos são escritos com parênteses vazios, tanto para construir quanto para casar: Nothing(), Nil().

Maybe

is_just(maybe) → boolean
is_nothing(maybe) → boolean

Testam qual forma um Maybe tem.

is_just(Just(5))
-- Saída: true
is_nothing(Nothing())
-- Saída: true
from_maybe(padrão, maybe) → valor

Desembrulha um Just, ou cai para padrão quando é Nothing.

from_maybe(0, Just(42))
-- Saída: 42
from_maybe(0, Nothing())
-- Saída: 0
maybe(padrão, função, maybe) → valor

Reduz um Maybe a um valor puro: aplica função ao conteúdo de um Just, senão devolve padrão.

maybe(-1, \x -> x * 2, Just(10))
-- Saída: 20
maybe(-1, \x -> x * 2, Nothing())
-- Saída: -1
map_maybe(função, maybe) → maybe

Transforma o valor dentro de um Just; um Nothing passa intacto.

map_maybe(\x -> x + 1, Just(7))
-- Saída: Just(8)
map_maybe(\x -> x + 1, Nothing())
-- Saída: Nothing()
bind_maybe(maybe, função) → maybe

Encadeia uma função que devolve Maybe. A cadeia curto-circuita para Nothing assim que qualquer passo produz um.

bind_maybe(Just(4), \x -> Just(x * x))
-- Saída: Just(16)
bind_maybe(Nothing(), \x -> Just(x))
-- Saída: Nothing()

Either

is_left(either) → boolean
is_right(either) → boolean

Testam qual lado um Either guarda.

is_right(Right(1))
-- Saída: true
is_left(Left("bad"))
-- Saída: true
either(no_left, no_right, either) → valor

Reduz um Either tratando ambos os lados, cada um com uma função.

either(\e -> "erro: " + e, \x -> "ok", Left("bad"))
-- Saída: "erro: bad"
either(\e -> 0, \x -> x, Right(9))
-- Saída: 9
map_either(função, either) → either

Mapeia sobre o lado do sucesso (Right); um Left passa intacto.

map_either(\x -> x + 1, Right(9))
-- Saída: Right(10)
map_either(\x -> x + 1, Left("e"))
-- Saída: Left(e)
map_left(função, either) → either

Mapeia sobre o lado da falha (Left); um Right passa intacto.

map_left(\e -> e + "!", Left("oops"))
-- Saída: Left(oops!)
bind_either(either, função) → either

Encadeia sobre o lado Right; o primeiro Left curto-circuita.

bind_either(Right(2), \x -> Right(x * 10))
-- Saída: Right(20)
bind_either(Left("stop"), \x -> Right(x))
-- Saída: Left(stop)
from_right(padrão, either) → valor
from_left(padrão, either) → valor

Desembrulham um lado, caindo para padrão no outro.

from_right(0, Right(5))
-- Saída: 5
from_right(0, Left("x"))
-- Saída: 0
either_to_maybe(either) → maybe
maybe_to_either(erro, maybe) → either

Convertem entre os dois. Um Left vira Nothing e um Right vira Just; no caminho inverso, um Nothing vira Left(erro).

either_to_maybe(Right(3))
-- Saída: Just(3)
maybe_to_either("none", Nothing())
-- Saída: Left(none)

bind genérico & operadores

bind e mmap despacham pelo construtor, então funcionam uniformemente sobre Maybe e Either - é isso que deixa os operadores agnósticos de tipo.

bind(monad, função) → monad

Bind monádico genérico para um Maybe ou um Either.

bind(Just(5), \x -> Just(x + 1))
-- Saída: Just(6)
bind(Right(5), \x -> Right(x + 1))
-- Saída: Right(6)
mmap(função, monad) → monad

Map de functor genérico para um Maybe ou um Either.

mmap(\x -> x * 3, Just(4))
-- Saída: Just(12)
função <$> monad → monad

Forma de operador do mmap: mapeia uma função pura sobre um valor monádico.

(\x -> x + 100) <$> Just(1)
-- Saída: Just(101)
monad >>= função → monad

Forma de operador do bind: alimenta um valor monádico para uma função monádica. Associativo à esquerda, então as cadeias se leem de cima para baixo.

Just(5) >>= \x -> Just(x + 1) >>= \y -> Just(y * 2)
-- Saída: Just(12)

Left("stop") >>= \x -> Right(x)
-- Saída: Left(stop)

O ADT List

Uma lista encadeada construída a partir de Cons(cabeça, cauda) e Nil(). A Bern já tem uma lista embutida rápida ([1, 2, 3]); este ADT serve para ensinar a forma e para código que faz pattern matching da estrutura. As pontes convertem de e para listas embutidas.

list_from(lista_embutida) → List
list_to(List) → lista_embutida

Convertem entre uma lista embutida e o ADT Cons/Nil.

list_to(list_from([1, 2, 3]))
-- Saída: [1, 2, 3]
list_map(função, List) → List

Aplica uma função a cada elemento.

list_to(list_map(\x -> x * x, list_from([1, 2, 3])))
-- Saída: [1, 4, 9]
list_foldr(função, acumulador, List) → valor

Reduz pela direita; função recebe (elemento, acumulador).

list_foldr(\a, b -> a + b, 0, list_from([1, 2, 3, 4]))
-- Saída: 10
list_length(List) → int
list_length(list_from([4, 5, 6, 7]))
-- Saída: 4
list_append(List, List) → List
list_to(list_append(list_from([1, 2]), list_from([3, 4])))
-- Saída: [1, 2, 3, 4]
list_reverse(List) → List

Reverte via um acumulador tail-recursivo - O(n) com prepends O(1).

list_to(list_reverse(list_from([1, 2, 3])))
-- Saída: [3, 2, 1]

Travessias

Percorrem uma lista embutida, rodam um passo com efeito em cada elemento e coletam os resultados - parando no primeiro erro. Elas dependem de uma lista poder conter os dois construtores de um ADT (ex.: [Just(1), Nothing()]), o que a Bern 2.2 permite.

sequence_maybe(lista_de_maybes) → maybe

Transforma uma lista de Maybes num Maybe de uma lista: Just de todos os valores se cada elemento for Just, senão Nothing.

sequence_maybe([Just(1), Just(2), Just(3)])
-- Saída: Just([1, 2, 3])
sequence_maybe([Just(1), Nothing(), Just(3)])
-- Saída: Nothing()
traverse_maybe(função, lista) → maybe

Mapeia uma função que devolve Maybe pela lista, falhando rápido no primeiro Nothing.

traverse_maybe(\x -> Just(x + 1), [1, 2, 3])
-- Saída: Just([2, 3, 4])
sequence_either(lista_de_eithers) → either
traverse_either(função, lista) → either

As versões de Either: Right de todos os valores, ou o primeiro Left encontrado.

sequence_either([Right(1), Left("oops"), Right(3)])
-- Saída: Left(oops)

Juntando tudo

As peças se compõem em pipelines que nunca quebram com um valor ausente ou inválido. Aqui algumas entradas são parseadas, validadas e combinadas - qualquer falha colapsa a cadeia inteira para Nothing:

Uma computação que pode falhar

import monads

def safe_div(a, b) do
    if (b == 0) then
        return Nothing()
    else
        return Just(a / b)
    end
end

result = Just(100)
    >>= \x -> safe_div(x, 5)
    >>= \y -> safe_div(y, 2)

from_maybe(-1, result)
-- Saída: 10   (100 / 5 = 20, depois 20 / 2 = 10)

from_maybe(-1, Just(100) >>= \x -> safe_div(x, 0))
-- Saída: -1   (a divisão por zero curto-circuita)

E uma travessia que valida uma lista inteira de uma vez, guardando o porquê na falha com Either:

Validando cada item

import monads

def check_positive(n) do
    if (n > 0) then
        return Right(n)
    else
        return Left("não positivo: " + n)
    end
end

from_right([], traverse_either(check_positive, [3, 8, 5]))
-- Saída: [3, 8, 5]

from_left("?", traverse_either(check_positive, [3, 0, 5]))
-- Saída: "não positivo: 0"