Mônadas em Coq
Published:
Introdução
Mônadas são uma maneira de estruturar computações que envolvem efeitos colaterais em linguagens de programação funcional. O conceito de mônada foi desenvolvido na matemática e utilizado, primeiramente, em semântica denotacional para formalização de efeitos colaterais. O uso de mônadas foi popularizado pela linguagem Haskell que as utiliza para estruturar programas que envolvem I/O e manipulação de estado entre outros tipos de efeitos colaterais.
Nesse post apresentaremos como classes de tipos em Coq podem ser utilizadas para definir mônadas e alguns exemplos de como essas podem estruturar programas envolvendo efeitos diversos.
Definindo uma mônada
De maneira simples, uma mônada é um construtor de tipos (mais precisamente, uma função M : Type -> Type
) que possui duas operações:
ret : A -> M A
, que converte um valorA
qualquer em uma computação que retorna, como resultado um valor desse tipo.bind : M A -> (A -> M B) -> M B
, que compõe duas computações permitindo que a segunda utilize o resultado da primeira.
Abaixo apresentamos a definição de mônada como uma classe de tipos e notações para facilitar sua utilização.
Class Monad (M : Type -> Type) : Type
:={
ret : forall {A : Type}, A -> M A
; bind : forall {A B : Type}, M A -> (A -> M B) -> M B
}.
Notation "x <- c1 ;; c2" := (bind c1 (fun x => c2))
(right associativity, at level 84, c1 at next level).
Notation "c1 ;; c2" := (bind c1 (fun _ => c2)) (at level 100, right associativity).
A mônada option
Um exemplo de instância para essa classe usa o tipo option
. De maneira simples, essa mônada (option) define computações que podem falhar.
Instance option_monad : Monad option
:={
ret := fun {A : Type}(x : A) => Some x
; bind := fun {A B : Type}(x : option A)(f : A -> option B) =>
match x with
| None => None
| Some y => f y
end
}.
Um possível uso dessa mônada seria para definir a operação de subtração sobre números naturais.
Fixpoint subtract (n m : nat) : option nat :=
match n, m with
| n' , 0 => ret n'
| 0 , _ => None
| S n', S m' => subtract n' m'
end.
Usando a mônada definida, podemos escrever de maneira simples um avaliador de expressões aritméticas que usam a subtração.
Inductive exp : Set :=
| Num : nat -> exp
| Plus : exp -> exp -> exp
| Minus : exp -> exp -> exp.
Fixpoint eval (e : exp) : option nat :=
match e with
| Num n => ret n
| Plus e1 e2 =>
n1 <- eval e1 ;;
n2 <- eval e2 ;;
ret (n1 + n2)
| Minus e1 e2 =>
n1 <- eval e1 ;;
n2 <- eval e2 ;;
subtract n1 n2
end.
A mônada err
Apesar de útul para definir computações parciais, a mônada option possui um inconveniente: ela não permite que informações sobre o tipo de falha sejam retornas pela computação que a ocasionou. Uma outra possibilidade para a modelagem deste tipo de situação é usar o tipo de dados err
.
Inductive err (A : Type) : Type :=
| Ok : A -> err A | Fail : string -> err A.
O construtor Ok
denota o sucesso, retornando um valor de tipo A
e Fail
denota a falha, armazendo uma string que a descreve. Podemos definir uma instância de Monad
para err
de maneira similar à mônada para option
.
Instance err_monad : Monad err
:={
ret := fun {A : Type}(x : A) => Ok x
; bind := fun {A B : Type}(x : err A)(f : A -> err B) =>
match x with
| Ok y => f y
| Fail s => Fail s
end
}.
Utilizando a mônada err
, podemos interpretar as expressões aritméticas indicando quando ocorre um erro de “underflow”.
Fixpoint eval' (e : exp) : err nat :=
match e with
| Num n => ret n
| Plus e1 e2 =>
n1 <- eval' e1 ;;
n2 <- eval' e2 ;;
ret (n1 + n2)
| Minus e1 e2 =>
n1 <- eval' e1 ;;
n2 <- eval' e2 ;;
match subtract n1 n2 with
| None => Fail "underflow"
| Some m => ret m
end
end.
A mônada de estado
A mônada de estado permite utilizarmos valores que podem ser modificados durante a execução de uma computação. Mostraremos como utilizar essa mônada para implementar um interpretador para uma linguagem que provê suporte a variáveis.
Primeiramente, definimos variáveis como sendo do string
.
Definition var := string.
A sintaxe de expressões possui variáveis, soma, multiplicação, atribuição (Set_s
), sequência (Seq_s
) e um condicional.
Inductive exp_s : Type :=
| Var_s : var -> exp_s
| Plus_s : exp_s -> exp_s -> exp_s
| Times_s : exp_s -> exp_s -> exp_s
| Set_s : var -> exp_s -> exp_s
| Seq_s : exp_s -> exp_s -> exp_s
| If0_s : exp_s -> exp_s -> exp_s -> exp_s.
Para representação do estado, ou seja, do valor associado a cada uma das variáveis utilizadas, utilizaremos uma função de tipo var -> nat
.
Definition state := var -> nat.
Na sequência, definimos uma função para atualizar o valor de uma variável em um estado. Utilizamos a função string_dec
, da biblioteca de Coq, que realiza o teste de igualdade de duas strings.
Definition upd(x:var)(n:nat)(s:state) : state :=
fun v => if string_dec x v then n else s x.
De posse dessa infraestrutura, podemos definir o tipo que será utilizado pela mônada de estado. De maneira simples, a mônada de estado é uma função que a partir do estado atual produz um par formado por um (possivelmente) novo estado e resultado da computação realizada. A definição da mônada simplesmente faz a composição de computações passando o estado atualizado para a próxima computação na sequência.
Definition state_comp (A:Type) := state -> (state * A).
Instance state_monad : Monad state_comp := {
ret := fun {A:Type} (x:A) => (fun (s:state) => (s,x)) ;
bind := fun {A B:Type} (c : state_comp A) (f: A -> state_comp B) =>
fun (s:state) =>
let (s',v) := c s in
f v s'
}.
Visando facilitar a definição do interpretador, vamos definir funções para a leitura e escrita de valores em variáveis.
Definition read (x:var) : state_comp nat :=
fun s => (s, s x).
Definition write (x:var) (n:nat) : state_comp nat :=
fun s => (upd x n s, n).
O intepretador para a linguagem de expressões possui implementação direta.
Fixpoint eval_sm (e:exp_s) : state_comp nat :=
match e with
| Var_s x => read x
| Plus_s e1 e2 =>
n1 <- eval_sm e1 ;;
n2 <- eval_sm e2 ;;
ret (n1 + n2)
| Times_s e1 e2 =>
n1 <- eval_sm e1 ;;
n2 <- eval_sm e2 ;;
ret (n1 * n2)
| Set_s x e =>
n <- eval_sm e ;;
write x n
| Seq_s e1 e2 =>
_ <- eval_sm e1 ;;
eval_sm e2
| If0_s e1 e2 e3 =>
n <- eval_sm e1 ;;
match n with
| 0 => eval_sm e2
| _ => eval_sm e3
end
end.
Conclusão
Nesse post apresentamos como mônadas podem ser definidas em Coq usando o mecanismo de classes de tipos. Foram implementadas três versões de intepretadores para expressões possuindo diferentes tipos de efeitos colaterais e como esses podem ser desenvolvidos como instâncias da classe Monad
apresentada.
Um ponto não desenvolvido nesse post é como demonstrar propriedades de mônadas. Para um tipo M
ser considerado uma mônada, esse deve possuir operações ret
e bind
de forma que:
ret
é identidade a direita e a esquerda parabind
.bind
é associativo.
De maneira simples, essas operações devem formar um monóide. Abaixo apresentamos uma classe de tipos para representar propriedades de uma mônada e a prova de que a mônada option
as satisfaz.
Todo o código desenvolvido nesse post está disponível no seguinte gist.
Class Monad_with_Laws (M: Type -> Type){MonadM : Monad M} :=
{
m_left_id : forall {A B:Type} (x:A) (f:A -> M B), bind (ret x) f = f x ;
m_right_id : forall {A B:Type} (c:M A), bind c ret = c ;
m_assoc : forall {A B C} (c:M A) (f:A->M B) (g:B -> M C),
bind (bind c f) g = bind c (fun x => bind (f x) g)
}.
Instance OptionMonadLaws : @Monad_with_Laws option _ := {}.
auto.
intros ; destruct c ; auto.
intros ; destruct c ; auto.
Defined.