Programação Funcional Aulas 9, 10 & 11 Sandra Alves DCC/FCUP 2015/16 1 Programas interativos Motivação Até agora apenas escrevemos programas que efetuam computação pura, i.e., transformações funcionais entre valores. Vamos agora ver como escrever programas interativos: lêm informação do teclado, ficheiros, etc.; escrevem no terminal ou em ficheiros;... Ações de I/O Introduzimos um novo tipo IO () para ações que, se forem executadas, fazem entrada/saída de dados. Exemplos: putchar A :: IO () putchar B :: IO () putchar :: Char -> IO () -- imprime um A -- imprime um B -- imprimir um carater 1
Encadear ações Podemos combinar duas ações de I/O usando o operador de sequenciação: (>>) :: IO () -> IO () -> IO () Exemplos: (putchar A >> putchar B ) :: IO () (putchar B >> putchar A ) :: IO () Note que >> é associativo mas não é comutativo! Em alternativa podemos usando a notação-do: -- imprimir AB -- imprimir BA = putchar A >> putchar B >> putchar C do {putchar A ; putchar B ; putchar C } Podemos omitir os sinais de pontuação usando a indentação: do putchar A putchar B putchar C Execução Para efetuar as ações de I/O definimos um valor main no módulo Main. module Main where main = do putchar A putchar B Compilar e executar: $ ghc Main.hs -o prog $./prog AB$ Também podemos efetuar ações IO diretamente no ghci: Prelude> putchar A >> putchar B ABPrelude> Definir novas ações Vamos agora definir novas ações de I/O combinando ações mais simples. Exemplo: definir putstr usando putchar recursivamente. putstr :: String -> IO () putstr [] =?? putstr (x:xs) = putchar x >> putstr xs Como completar? 2
Ação vazia putstr :: String -> IO () putstr [] = return () putstr (x:xs) = putchar x >> putstr xs return () é a ação vazia: se for efetuada, não faz nada. Mais geralmente IO a é o tipo de ações que, se forem executadas, fazem entrada/saída de dados e devolvem um valor de tipo a. Exemplos: putchar A :: IO () getchar :: IO Char -- escrever um A ; resultado vazio -- ler um caracter; resultado Char Ações IO pré-definidas getchar :: IO Char getline :: IO String getcontents :: IO String putchar :: Char -> IO () putstr :: String -> IO () putstrln :: String -> IO () print :: Show a => a -> IO () return :: a -> IO a -- ler um caracter -- ler uma linha -- ler toda a entrada padrão -- escrever um carater -- escrever uma linha de texto -- idem com mudança de linha -- imprimir um valor -- ação vazia Combinando leitura e escrita Usamos <- para obter valores retornados por uma ação I/O. Exemplo: ler e imprimir caracteres até obter um fim-de-linha. main :: IO () main = do x<-getchar putchar x if x== \n then return () else main Outro exemplo: boasvindas :: IO () boasvindas = do putstr "Como te chamas? " nome <- getline putstr ("Bem-vindo, " ++ nome ++ "!\n") 3
Valores de retorno Podemos usar return para definir valores de retorno de ações. boasvindas :: IO String boasvindas = do putstr "Como te chamas? " nome <- getline putstr ("Bem-vindo, " ++ nome ++ "!\n") return nome Outro exemplo: definir getline usando getchar. getline :: IO String getline = do x<-getchar if x== \n then return [] else do xs<-getline return (x:xs) Jogo Hi-Lo Exemplo maior: um jogo de perguntas-respostas. o computador escolhe um número secreto entre 1 e 100; o jogador vai fazer tentativas de advinhar; para cada tentativa o computador diz se é alto ou baixo; a pontuação final é o número de tentativas. Tentativa? 50 Demasiado alto! Tentativa? 25 Demasiado baixo! Tentativa? 35 Demasiado alto! Tentativa? 30 Demasiado baixo! Tentativa? 32 Acertou em 5 tentativas. Vamos decompor em duas partes: main escolhe o número secreto e inicia o jogo; jogo função recursiva que efetua a sequência perguntas-respostas. Programa module Main where import Data.Char(isDigit) import System.Random(randomRIO) 4
main = do x <- randomrio (1,100) -- escolher número aletório n <- jogo 1 x -- começar o jogo putstrln ("Acertou em " ++ show n ++ " tentativas") jogo :: Int -> Int -> IO Int jogo n x = do { putstr "Tentativa? " ; str <- getline ; if all isdigit str then let y = read str in if y>x then do putstrln "Demasiado alto!"; jogo (n+1) x else if y<x then do putstrln "Demasiado baixo!"; jogo (n+1) x else return n else do putstrln "Tentativa inválida!"; jogo n x } -- n: tentativas, x: número secreto Ações são valores As ações IO são valores de primeira classe: podem ser argumentos ou resultados de funções; podem passados em listas ou tuplos;... Isto permite muita flexibilidade ao combinar ações. Exemplo: uma função para efetuar uma lista de ações por ordem. seqn :: [IO a] -> IO () seqn [] = return () seqn (m:ms) = m >> seqn ms Exemplos de uso: > seqn [putstrln s s<-["ola", "mundo"]] ola mundo > seqn [print i i<-[1..5]] 1 2 3 4 5 Sumário Programas reais necessitam de combinar interação e computação pura Em Haskell fica explícito nos tipos quais as funções que fazem interação e quais são puras. 5
A notação-do e o tipo IO é usada para: ler e escrever no terminal e em ficheiros; estabelecer comunicações de rede; serviços do sistema operativo (ex: obter data e hora do relógio de sistema); A notação-do pode usada para outras computações não puramente funcionais: estado, não-determinismo, exceções, etc. 2 Definição de tipos Declarações de sinónimos Podemos dar um nome novo a um tipo existente usando uma declaração de sinónimo. Exemplo (do prelúdio-padrão): type String = [Char] As declarações de sinónimos são usadas para melhorar legibilidade de programas. Exemplo: type Pos = (Int,Int) type Cells = [Pos] -- coluna,linha -- colónia Assim podemos escrever isalive :: Cells -> Pos -> Bool em vez de isalive :: [(Int,Int)] -> (Int,Int) -> Bool As declarações de sinónimos também podem ter parâmetros. Exemplo: associações entre chaves e valores. type Assoc ch v = [(ch,v)] -- tabela de associações idades :: Assoc String Int idades = [("Sara", 39), ("Jo~ao", 27), ("Maria", 19)] emails :: Assoc String String emails = [("Sandra", "sandra@dcc.fc.up.pt"), ("Jo~ao", "joao@gmail.com")] Os sinónimos podem ser usados noutras definições: type Pos = (Int,Int) type Cells = [Pos] -- OK Mas não podem ser usados recursivamente: type List a = (a,list a) -- ERRO 6
Declarações de novos tipos Podemos definir novos tipos de dados usando declarações data. Exemplo (do prelúdio-padrão): data Bool = True False A declaração data enumera as alternativas separadas por barras verticais. Cada alternativa deve ter um construtor (ex.: True e False). O nome dos tipos e construtores deve ser começar por uma letra maiúscula. Cada construtor só pode ser usado num único tipo. Podemos definir funções sobre novos tipos usando padrões. Exemplo: um tipo para as direções ortogonais (esquerda, direita, cima, baixo). data Dir = Esq Dir Cima Baixo Vamos definir algumas funções... contraria :: Dir -> Dir contraria Esq = Dir contraria Dir = Esq contraria Cima = Baixo contraria Baixo = Cima mover :: Dir -> Pos -> Pos mover Esq (x,y) = (x-1,y) mover Dir (x,y) = (x+1,y) mover Cima (x,y) = (x,y+1) mover Baixo (x,y)= (x,y-1) -- direção contrária -- deslocar numa direção Construtores com parâmetros Os construtores podem também ter parâmetros. Exemplo: data Figura = Circ Float -- raio Rect Float Float -- largura, altura quadrado :: Float -> Figura quadrado h = Rect h h area :: Figura -> Float area (Circ r) = pi*r^2 area (Rect w h) = w*h Os construtores podem ter diferentes números de parâmetros 7
Os parâmetros podem ser de tipos diferentes Podemos usar os construtores de duas formas: como funções para construir um valor Circ :: Float -> Figura Rect :: Float -> Float -> Figura em padrões no lado esquerdo de equações area (Circ r) = pi*r^2 area (Rect w h) = w*h Igualdade e conversão em texto Por omissão um novo tipo não tem métodos de igualdade ou conversão para texto. O interpretador dá erro se tentarmos mostrar ou comparar valores: > Circ 2 ERROR: No instance for (Show Figura)... > Rect 2 1 == Rect 1 2 ERROR: No instance for (Eq Figura)... Podemos definir igualdade e conversão para texto automaticamente usando deriving : data Figura = Circ Float Rect Float Float deriving (Eq, Show) Exemplo de uso: > Circ 2 Circ 2.0 > Rect 2 1 == Rect 1 2 False A igualdade é sintática: dois valores são iguais se e só se têm o mesmo construtor e argumentos. Novos tipos com parâmetros As declarações de novos tipos também podem ter parâmetros. Exemplo: data Maybe a = Nothing Just a -- do prelúdio-padrão safediv :: Int -> Int -> Maybe Int safediv _ 0 = Nothing safediv n m = Just (n div m) safehead :: [a] -> Maybe a safehead [] = Nothing safehead xs = Just (head xs) 8
Tipos recursivos As declarações data podem ser recursivas. Exemplo: os números naturais. data Nat = Zero Suc Nat Alguns valores de Nat: Zero Suc Zero Suc (Suc Zero) Suc (Suc (Suc Zero)). -- zero -- um -- dois -- três Em geral: n é obtido aplicado n vezes Succ a Zero. Suc (Suc (... (Suc Zero)...)) Usando recursão, podemos definir funções que convertem entre inteiros e naturais: -- n aplicações int2nat :: Int -> Nat int2nat 0 = Zero int2nat n n>0 = Suc (int2nat (n-1)) nat2int :: Nat -> Int nat2int Zero = 0 nat2int (Suc n)= 1+nat2int n Podemos usar as funções de conversão para somar naturais. add :: Nat -> Nat -> Nat add n m = int2nat (nat2int n + nat2int m) Em alternativa, podemos definir a soma usando recursão sobre naturais. add :: Nat -> Nat -> Nat add Zero m = m add (Suc n) m = Suc (add n m) Estas duas equações traduzem as seguintes igualdades algébricas: = = = 0 + m = m (1 + n) + m = 1 + (n + m) Exemplo: add (Suc (Suc Zero)) (Suc Zero) Suc (add (Suc Zero) (Suc Zero)) Suc (Suc (add Zero (Suc Zero))) Suc (Suc (Suc Zero)) 9
Árvores sintáticas Podemos representar expressões por uma árvore sintática em que os operadores são os nós e as constantes são as folhas. Exemplo: 1 + 2 3 + 1 2 3 As árvores podem ser representadas em Haskell por um tipo recursivo. data Expr = Val Int -- constante Soma Expr Expr -- nó + Mult Expr Expr -- nó A árvore no slide anterior é: Soma (Val 1) (Mult (Val 2) (Val 3)) Exemplos de funções sobre árvores de expressões. -- contar o número de folhas tamanho :: Expr -> Int tamanho (Val n) = 1 tamanho (Soma e1 e2) = tamanho e1 + tamanho e2 tamanho (Mult e1 e2) = tamanho e1 + tamanho e2 -- calcular o valor valor :: Expr -> Int valor (Val n) = n valor (Soma e1 e2) = valor e1 + valor e2 valor (Mult e1 e2) = valor e1 * valor e2 Árvores binárias Também podemos usar árvores binárias para facilitar a organização e pesquisa de informação. 3 5 1 4 6 9 Podemos representar árvores binárias de inteiros por um tipo recursivo. data Arv = Folha Int No Arv Int Arv 7 10
A árvore no slide anterior seria representa por: No (No (Folha 1) 3 (Folha 4)) 5 (No (Folha 6) 7 (Folha 9)) Podemos agora definir uma função recursiva para procurar um valor numa árvore. ocorre :: Int -> Arv -> Bool ocorre m (Folha n) = n==m ocorre m (No esq n dir) = (n==m ocorre m esq ocorre m dir) Numa árvore ordenada todos os nós têm valores inferiores na sub-árvore esquerda e superiores na subárvore direira. Nesse caso podemos simplificar a pesquisa: ocorre :: Int -> Arv -> Bool ocorre m (Folha n) = n==m ocorre m (No esq n dir) n==m = True m<n = ocorre m esq m>n = ocorre m dir Esta definição é mais eficiente: percorre apenas os nós num caminho da raiz até uma folha em vez de todos os nós da árvore. 3 Verificador de tautologias Proposições lógicas Uma proposição lógica é construida apartir de: constantes T, F (verdade e falsidade) variáveis a, b, c,... conectivas lógicas,,, = parêntesis (, ) Exemplos: Tabelas de verdade das conectivas a b a (( a) = F ) ( (a b)) = (( a) ( b)) a b a b F F F T F F F T F T T T a b a b F F F T F T F T T T T T a a F T T F a b a = b F F T T F F F T T T T T 11
Tautologias Uma proposição cujo valor é verdade para qualquer atribuição de valores às variáveis diz-se uma tautologia. Exemplo: a a a a F T T T F T Conclusão: a a é uma tautologia. Representação de proposições Vamos definir um tipo recursivo para representar proposições. data Prop = Const Bool -- constantes Var Char -- variáveis Neg Prop -- negação Conj Prop Prop -- conjunção Disj Prop Prop -- disjunção Impl Prop Prop -- implicação deriving (Eq,Show) Exemplo: a proposição é representada como a = (( a) = F ) Impl (Var a ) (Impl (Neg (Var a )) (Const False)) Associação de valores a variáveis Para atribuir valores de verdade às variáveis vamos usar uma lista de associações. Exemplo: a atribuição é representada pela lista [( a,true),( b,false),( c,true)] Definimos: listas de associações entre chaves e valores; a = T b = F c = T uma função para procurar o valor associado a uma chave. type Assoc ch v = [(ch,v)] find :: Eq ch => ch -> Assoc ch v -> v find ch assocs = head [v (ch,v)<-assocs, ch==ch ] É uma função parcial: dá um erro se não encontrar a chave! 12
Calcular o valor duma proposição Vamos definir o valor de verdade de uma proposição por recursão. O primeiro argumento é uma atribuição de valores às variáveis. type Atrib = Assoc Char Bool valor :: Atrib -> Prop -> Bool valor s (Const b) = b valor s (Var x) = find x s valor s (Neg p) = not (valor s p) valor s (Conj p q) = valor s p && valor s q valor s (Disj p q) = valor s p valor s q valor s (Impl p q) = not (valor s p) valor s q Gerar atribuições às variáveis Para n variáveis distintas há 2 n linhas na tabela de verdade. Como obter todas as atribuições de forma sistemática? Vamos escrever uma função para gerar todas as sequências de n boleanos (cf. exercício 46): bits :: Int -> [[Bool]] Exemplo, as sequências de comprimento 3 (três variáveis): F F F F F T F T F F T T bits 3 T F F T F T T T F T T T Podemos decompor em duas cópias da tabela para 2 variáveis com uma coluna extra: F F F F F T bits 2 F T F F T T T F F T F T bits 2 T T F T T T Em geral: vamos gerar as sequências de forma recursiva. bits :: Int -> [[Bool]] bits 0 = [[]] bits n = [b:bs bs<-bits (n-1), b<-[false,true]] Falta ainda gerar atribuições; começamos por listar todas as variáveis numa proposição. 13
vars :: Prop -> [Char] vars (Const _) = [] vars (Var x) = [x] vars (Neg p) = vars p vars (Conj p q) = vars p ++ vars q vars (Disj p q) = vars p ++ vars q vars (Impl p q) = vars p ++ vars q A função seguinte gera todas as as atribuições de variáveis duma proposição: atribs :: Prop -> [Atrib] atribs p = map (zip vs) (bits (length vs)) where vs = nub (vars p) (A função nub da biblioteca Data.List remove repetidos.) Verificar tautologias Uma proposição é tautologia se e só se for verdade para todas as atribuições de variáveis. tautologia :: Prop -> Bool tautologia p = and [valor s p s<-atribs p] Alguns exemplos: > tautologia (Var a ) False > tautologia (Impl (Var p ) (Var p )) True > tautologia (Disj (Var a ) (Neg (Var a )) True Exercícios 1. Escrever uma função que calcula a lista das atribuições que tornam uma proposição falsa (i.e. uma lista de contra-exemplos). 2. Escrever um programa para imprimir a tabela de verdade duma proposição. 14