List Comprehension Classes de Tipos

Frank Coelho de Alcantara

List Comprehension

Frank Coelho de Alcantara

Uma Notação de Conjuntos

A linguagem Haskell trouxe para as linguagens de programação a sintaxe de definição de conjuntos.

$A = \{3x | x \in \mathbb{Z}, x < 3 \}$

Nesta definição, os elementos do conjunto são definidos por um predicado. Neste caso, teremos:

  • $x$: é a variável que representa os itens do conjunto;
  • $\mathbb {Z}$: representa o conjunto de entrada, neste caso, números inteiros;
  • $x < 3$: é parte do predicado e atua como se fora um filtro;
  • $3x$: é a regra de produção de itens do conjunto, que satisfazem o predicado;
  • $\{\}$: indica que o resultado será um conjunto;
  • $, \space$: a vírgula separa os predicados e pode ser lida como "E".

Em Haskell

            
                      module Main where
                      
                      -- [<output function> | <input set>, ..., <predicate>, ... ]
                      
                      main::IO()
                      main = do
                        print [ x*x | x <- [1..10], mod x 2==0 ] 
                        -- for x from 1 to 2 ... for y from 1 to 4
                        print "olhe este"
                        print [(x,y) | x <- [1..2], y <- [1..4] ] 
                        -- for y from 1 to 4 ... for x from 1 to 2
                        print "Invertido"
                        print [(x,y) | y <- [1..4], x <- [1..2] ] 
                                    
                        print [x^y | x <- [1..10], y <-[1..4], even x ] 
                        print [x*y | x <- [1..10], y <- [1..4], odd y ]
                    
          

Geradores múltiplos são como laços aninhados, o último gerador é o laço mais interno.

Classes de Tipos

Frank Coelho de Alcantara

Revendo

Classes de tipos definem um conjunto de funções que podem ter implementações diferentes de acordo com o tipo de dado.

            
              (+) :: Num a => a -> a -> a
            
          

Que deve ser lida como $(+)$ é do tipo $a \rightarrow a \rightarrow a$ para qualquer tipo numérico $a$. Uma classe de tipos, como é o caso da classe $Num$ contém um conjunto de métodos, como o $(+)$, que pode ser definida de forma diferente para cada instância da classe. A seta dupla $\Rightarrow$, representa a existência de uma restrição e chamamos esta restrição de contexto. Neste caso, a restrição diz que a função $(+)$ recebe dois valores do tipo $a$ e devolve um valor do tipo $a$ apenas se $a$ for da classe $Num$.

Para todo $a$ que seja uma instância da classe $Num$, então $(+)$ tem o tipo $a \rightarrow a \rightarrow a$.

Números Naturais

              
                module Main where
                
                -- definindo uma classe de tipos para números naturais
                data Nat = Zero | Succ Nat
                  
                -- quer dizer que a classe Nat tem dois construtores Zero e Succ e o
                -- Succ precisa de uma variável de tipo.
                -- funções aritméticas
                  
                instance Eq Nat where
                  Zero == Zero = True
                  Zero == Succ n = False
                  Succ m == Zero = False
                  Succ m == Succ n = (m == n)
                  
                instance Ord Nat where
                  Zero < Zero=False 
                  Zero < Succ n=True 
                  Succ m < Zero=False 
                  Succ m < Succ n=(m < n) 
                  Succ m <=Succ n=(m <=n) 
                  
                instance Show Nat where 
                  show Zero="Zero" 
                  show (Succ Zero)="Succ Zero" 
                  show (Succ (Succ n))="Succ (" ++ show (Succ n) ++ ")" 
                  
                {- para gerar o valor numérico substitua por 
                  
                  show x=show (converte x) where 
                    converte Zero=0 
                    converte (Succ x)=1 + converte x 
                
                -} 
                
                instance Num Nat where 
                  m + Zero=m 
                  m + Succ n=Succ (m + n) 
                  m * Zero=Zero m * (Succ n)=m*n+m 
                  m - Zero=m Zero - Succ n=Zero 
                  Succ m - Succ n=m - n 
                  
                  abs n=n 
                  -- devolve -1 para valores negativos, 0 para zero e 1 para positivo 
                  signum Zero=Zero 
                  signum (Succ n)=Succ Zero 
                  
                  -- para poder usar números literais inteiros em classe Num 
                  fromInteger x 
                    | x <=0=Zero 
                    | otherwise=Succ (fromInteger (x-1)) 
                
                soma :: Nat -> Nat -> Nat
                soma x y = x + y
                  
                produto :: Nat -> Nat -> Nat
                produto x y = x * y

                main::IO()
                main=do
                  print (soma 4 4)
                  print (produto 2 3)
              
            

O código pode ser encontrado aqui.

Records

            
              module Main where
              
              -- Trabalhando com registros
              {-
              data Funcionario = Funcionario String String Int Float String String deriving (Show)
              
              nome :: Funcionario -> String
              nome (Funcionario nome _ _ _ _ _) = nome
              
              sobrenome :: Funcionario -> String
              sobrenome (Funcionario _ sobrenome _ _ _ _) = sobrenome
              
              idade :: Funcionario -> Int
              idade (Funcionario _ _ idade _ _ _) = idade
              
              altura :: Funcionario -> Float
              altura (Funcionario _ _ _ altura _ _) = altura
              
              telefone :: Funcionario -> String
              telefone (Funcionario _ _ _ _ number _) = number
              
              cargo :: Funcionario -> String
              cargo (Funcionario _ _ _ _ _ cargo) = cargo
              -}
              
              data Funcionario = Funcionario { nome :: String
                              , sobrenome :: String
                              , idade :: Int
                              , altura :: Float
                              , telefone :: String
                              , cargo :: String
                              } deriving (Show, Eq)
              
              main=do
                let frank = Funcionario "Frank" "Alcantara" 57 1.80 "414141414141" "Diretor"
                let paulo = Funcionario "Paulo" "Alcantara" 57 1.80 "414141414141" "Diretor"
                let outrocara = Funcionario "Frank" "Alcantara" 57 1.80 "414141414141" "Diretor"
                
                print(frank)
                print(nome frank)
                print (frank == paulo)
                print (frank == outrocara)
                print (show frank)
            
          

O código pode ser encontrado aqui.

Exemplo 1 - tipos

Crie uma função que devolva, a partir de uma lista de estudantes, aquele que tem a maior nota. Observe que será necessário armazenar, o nome, o código de matrícula e a nota de cada um dos estudantes da lista. Para testar, crie uma lista com pelo menos quatro estudantes.

Uma solução pode ser encontrada aqui

Exemplo 2

Crie um programa que determine qual a pizza é mais barata considerando sua área. Sua função deve comparar no mínimo duas pizzas, cujos raios e preços serão indicados pelo usuário do programa.

Uma solução pode ser encontrada aqui

Classes de Tipos

                
                    class Num a where
                       (+) :: a -> a -> a
                       (-) :: a -> a -> a
                       (*) :: a -> a -> a
                        negate :: a -> a
                        abs :: a -> a
                        signum :: a -> a
                 
              

Uma classe é a definição de um família de funções que todos os membros da classe deverão implementar. Observe a variável de tipo.

Porque a divisão não está nesta lista?

Classes de Tipos Benefícios

Uma vez que você define uma função, esta função poderá ser aplicada a apenas um tipo específico.

Sem classe de tipos, você vai precisar definir uma função para cada tipo.

As classes de tipos permitem que você crie funções que poderão ser aplicadas a tipos que você nem sabe que existem.

                
                    someEDobre :: Num a => a -> a -> a
                    someEDobre x y = (x + y)*2
                 
                

Revisitando Ord e Eq

                
                    class Eq a => Ord a where
                      compare :: a -> a -> Ordering
                      (<) :: a -> a -> Bool
                      (<=) :: a -> a -> Bool
                      (>) :: a -> a -> Bool
                      (>=) :: a -> a -> Bool
                      max :: a -> a -> a
                      min :: a -> a -> a
                 
                

Na definição da classe há uma restrição de classe! Para começar a entender Ord precisamos entender Eq.

                
                    class Eq a where
                      (==) :: a -> a -> Bool
                      (/=) :: a -> a -> Bool
                 
                

Você só consegue ordenar itens de conjuntos que são fechados em relação as operações de igualdade e desigualdade. O contrário não é verdadeiro.

Bounded

                
                    GHCi> :info Int
                      data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in 'GHC.Types'
                      instance Bounded Int -- Defined in 'GHC.Enum'
                      instance Enum Int -- Defined in 'GHC.Enum'
                      instance Eq Int -- Defined in 'GHC.Classes'
                      instance Integral Int -- Defined in 'GHC.Real'
                      instance Num Int -- Defined in 'GHC.Num'
                      instance Ord Int -- Defined in 'GHC.Classes'
                      instance Read Int -- Defined in 'GHC.Read'
                      instance Real Int -- Defined in 'GHC.Real'
                      instance Show Int -- Defined in 'GHC.Show'
                 
                

Use para tipos que devem ter um limite superior e um limite inferior. Se você olhar o tipo $integer$ verá que este tipo não é $Bounded$.

                
                    class Bounded a where
                        minBound :: a
                        maxBound :: a
                 
                

Derivação

                
                    data Sorvete = Chocolate | Creme
                 
                

Se você olhar, este tipo $Sorvete$ é, para todos os efeitos idêntico ao tipo Bool:.

                
                    data Bool = False | True
                 
                

A diferença é que o Bool implementa uma instância de $show$. Então:

                
                    data Sorvete = Chocolate | Creme deriving (Show)
                 
                

Indo um pouco mais longe:

                
                    data Sorvete = Chocolate | Creme deriving (Show,Eq,Ord)
                 
                

Vamos Jogar Dados

                
                    data Dado = F1|F2|F3|F4|F5|F6 
                    instance Show Dado where
                        show F1 = "um"
                        show F2 = "dois"
                        show F3 = "três"
                        show F4 = "quatro"
                        show F5 = "cinco"
                        show F6 = "seis"
                    instance Eq Dado where
                        (==) F6 F6 = True
                        (==) F5 F5 = True
                        ...
                        (==) _ _ = False
                    instance Ord Dado where
                        compare F6 F6 = EQ
                        compare F6 _ = GT
                        compare _ F6 = LT  
                        ...  
                    instance Enum Dado where
                      toEnum 0 = F1
                      toEnum 1 = F2
                      ...
                      toEnum _ = error "Este valor não existe"
                      fromEnum F1 = 0
                      fromEnum F2 = 1
                      fromEnum F3 = 2
                      ...
                 
                

Um Pouco de Matemática

Frank Coelho de Alcantara

Relações

Uma relação é uma forma de criar um vínculo entre dois itens de um conjunto, ou entre itens de dois ou mais conjuntos. Quando a relação é determinada entre itens do mesmo conjunto ela é chamada de endorelação, ou relação endógina. Quando a relação é criada entre itens de dois conjuntos ela é chamada de relação binária.

A relação $\rho$ entre os conjuntos $\mathbb{S}$ e $\mathbb{T}$ será um subconjunto do produto cartesiano entre $\mathbb{S}$ e $\mathbb{T}$: $$x \rho y |(x,y)\in \rho \wedge \rho \subseteq \mathbb{S}\times \mathbb{T} $$

Operações entre Relações

Seja $\mathbb{B}$ o conjunto de todas as relações binárias em um conjunto $\mathbb{S}$ dado, teremos: $$\rho \in \mathbb{B}\wedge \sigma \in \mathbb{B}\therefore \rho \subseteq \mathbb{S}\times \mathbb{S}\wedge \sigma \subseteq \mathbb{S}\times \mathbb{S}$$

Desta forma, todas as operações realizadas entre $\rho$ e $\sigma$ resultam em subconjuntos de $\mathbb{S}\times \mathbb{S}$. Ou seja:

  • $(\rho \cup \sigma)\subseteq \mathbb{S}\times \mathbb{S}$
  • $(\rho \cap \sigma)\subseteq \mathbb{S}\times \mathbb{S}$
  • $(\rho')\subseteq \mathbb{S}\times \mathbb{S}$
  • $(\sigma')\subseteq \mathbb{S}\times \mathbb{S}$

Operações Lógicas e Fecho

Uma vez que tenham sido definidas as operações de união, interseção e complemento teremos:

  • $x (\rho \cup \sigma) y\leftrightarrow (x\rho y)\vee (x\sigma y)$
  • $x (\rho \cap \sigma) y\leftrightarrow(x\rho y)\wedge (x\sigma y)$
  • $x \rho' y\leftrightarrow \neg (x\rho y)$

Sejam $\rho : \mathbb{A} \rightarrow \mathbb{A}$ e um conjunto de propriedades $\mathbb{P}$ qualquer então o fecho de $\rho $ em relação a $\mathbb{A}$ é a menor endorrelação em $\mathbb{A}$ que contém $\rho $ e que satisfaz as propriedades de $\mathbb{P}$.

Exemplo: Fecho Simétrico $\rho \cup \rho^{-1}$

Monoide

Um $monoide$ é um conjunto fechado em relação a uma operação binária associativa $*:\mathbb{S}\times \mathbb{S}\rightarrow \mathbb{S}$ e uma função identidade $id \in \mathbb{S}$, geralmente a partir de um elemento neutro, de tal forma que para $a, b, c \in \mathbb{S}$ teremos:

  1. Fechamento (clojure): $a * b \in \mathbb{S}$;
  2. Associatividade: $a*(b*c)=(a*b)*c$;
  3. Identidade: $(\exists \space\space id) | (id*a\space\space =\space\space a*id =\space\space a)$.

Muitos conjuntos formam $monoides$ em relação a uma ou mais operações. Por exemplo, o conjunto dos inteiros $\mathbb{Z}$ forma $monoides$ na adição, na subtração e na multiplicação. Na adição e subtração com o $0$ e na multiplicação com o $1$. Um $monoide$ é um $semigrupo$ com identidade que, por sua vez, é um $magma$ com associatividade. finalmente adicionando invertibilidade ao $monoide$, chegamos ao $grupo$.

Exemplos

$Magma$: $(\mathbb{N}, (n,m) \rightarrow n^m)$ o conjunto $\mathbb{N}$ e a operação de potência $(n,m)\rightarrow n^m$. Esta operação não é associativa.

$Semigrupo$: $(A,*)$ onde $A ={a \in \mathbb{I} \wedge a \space mod \space 2 = 0}$ e $*$ indica a multiplicação. A associatividade está clara em $(a * b) * c = a * (b * c) | a,b,c \in A$. Não existe elemento neutro em $A$, nem função identidade em $(A,*)$.

$Monoide$: $(\mathbb{N},+)$ possui associatividade e identidade. Não tem invertibilidade.

$Grupo$: $(\mathbb{Z},+)$ possui associatividade e identidade. Tem invertibilidade: $$\forall (a \in \mathbb{Z}) \rightarrow (a + (-a) = 0) \wedge ((-a)+a=0)$$

Função

Função é a aplicação de uma relação entre dois conjuntos $domínio$ e $contradomínio$ em que, para cada elemento do $domínio$, existirá um correspondente no $contradomínio$, esse correspondente é conhecido como $imagem$. $$f:S \rightarrow T$$

Para definir a função precisamos:

  • A definição do Domínio;
  • A definição do Contradomínio;
  • Uma relação.

A relação pode ser uma descrição verbal, um gráfico, uma equação ou uma coleção de pares ordenados. Relações um-para-um e muitos-para-um.

Composição de Funções

Sejam as funções: $$f:S\rightarrow T \space\space \wedge \space\space g: T\rightarrow U$$

Então a função composta $g\bullet f$ é uma função de $S$ em $U$ definida por: $$(g\bullet f)(s)=g(f(s)) | s \in S$$

A função $g\bullet f$ é a composição de $g$ e $f$.

Esta é a origem dos diagramas comutativos.

Um exemplo pode ser encontrado aqui

Categorias

Uma categoria é um conjunto de objetos (elementos) e setas.

Os objetos podem ser representados como círculos, pontos, ou letras e setas são setas.

Uma categoria representa uma composição, ou vice-versa. As setas permitem criar a composição.

exemplo de categoria com uma seta partindo de S para T, uma de T para U e uma de S para U

Categorias Formalmente

Uma categoria é uma coleção de objetos $A, B, S, T, U...$. Frequentemente estes objetos representam estrutura complexas, como estruturas topológicas ou grupos. Entretanto, para nosso objetivo, são apenas vértices de um grafo direcionado. Pense em tipos.

Uma coleção de morfismos tipados $f:S\rightarrow T$ para cada par de objetos. Aqui também os objetos $S$ e $T$ serão chamados de $domínio$ e $contradomínio$ de $f$. $Morfismos$ são funções de $S$ para $T$. De forma abstrata morfismos são apenas os vértices de um grafo direcionado.

Um operador de composição $\circ$ tipado de tal forma que se existem $f:S\rightarrow T$ e $g:T\rightarrow U$ existe $g\circ f:S\rightarrow U$.

Um morfismo identidade $id:S\rightarrow S$ para cada objeto de $S$. Os morfismos identidade são identidades para composição à direita e à esquerda.

Exemplos: conjuntos, conjuntos de funções, grupos, espaços topológicos, espaços vetoriais em $\mathbb{R}$. Também seria um exemplo o conjunto, parcialmente ordenado $(X, \leq)$ composto de objetos $x$ tal que exista um morfismo $x \rightarrow y \space\space iff \space\space x \leq y$. Esta é uma categoria de ordenação. Ou seja, uma categoria é apenas um monoide tipado.

Functor

Um $Functor$ é um mapeamento entre categorias que preserva suas estruturas.

Se $C$ e $D$ são categorias, um Functor $F:C\rightarrow D$ mapeia objetos $A \in C$ em objetos $FA \in D$ e os morfismos $f: A\rightarrow B$ de $C$ nos morfismos $Ff: FA \rightarrow FB$ de $D$ de tal forma que $F(g \circ f) = Fg \circ Ff$ e $F(idA)= idFA.

Functors são morfismos de categorias. O que pode ser entendido com sendo uma categoria de categorias. Apenas tome cuidado com esta metáfora já que podemos incorrer em alguns dos paradoxos da teoria dos conjuntos.

Um Functor mapeias objetos e morfismos de uma categoria em outra, preservando os morfismos originais. Ou, em outras palavras, as composições são preservadas: Se aplicamos o Functor $F$ a composição $h = g\circ f $ teremos $Fh= Fg \circ Ff$.

Functor em Haskell

                
                    class Functor f where  
                          fmap :: (a -> b) -> f a -> f b  
                 
                

O tipo de $f$ é abstrato, ao contrário dos tipos $a$ que vimos até o momento. Neste caso, $f$ não é uma variável de tipos, é um constructor de tipos que receberá, como parâmetro, um tipo.

$fmap$ recebe uma função $a\rightarrow b$ que recebe um tipo e devolve outro e um functor aplicado sobre um tipo $fa$ e devolve um functor aplicado ao outro tipo $fb$.

                
                    class Functor f where  
                          map :: (a -> b) -> [a] -> [b]  
                 
                

A classe $Functor$ define uma função, $fmap$, mas não fornece nenhuma implementação.

Um exemplo em Haskell pode ser encontrado aqui

Monoid

Formalmente o momoide é um primitivo da álgebra abstrata que define um conjunto fechado em relação a uma operação de tal forma que esta operação seja binária, associativa e possua um elemento identidade.

Em Haskell a $monoid$ é um tipo que define uma regra para definir como dois elementos daquele tipo podem ser combinados para definir um novo elemento do mesmo tipo.

A função $++$ é um bom exemplo, eu posso usar em $[1,2]$ e $[3,4]$ para obter $[1,2,3,4]$ no conjunto dos inteiros a operação soma é outro bom exemplo.

                
                    class Monoid a where
                            mempty  :: a
                            -- identidade de mappend
                            mappend :: a -> a -> a
                            -- associatividade de mappend
                            mconcat :: [a] -> a

                 
                

Ou, com considerando $++$ poderíamos ter:

                
                    instance Monoid [a] where
                      mappend = (++)
                      mempty = []
                      a `mappend` mempty = a
                      mempty `mappend` a = a
                 
                

Um detalhamento dos monoides em Haskell pode ser encontrado aqui

Functor: no Haskell

Um $Functor$ é um map entre categorias que preserva suas estruturas.

Se $C$ e $D$ são categorias, um Functor $F:C\rightarrow D$ mapeia objetos $A \in C$ em objetos $FA \in D$ e os morfismos $f: A\rightarrow B$ de $C$ nos morfismos $Ff: FA \rightarrow FB$ de $D$ de tal forma que $F(g \circ f) = Fg \circ Ff$ e $F(idA)= idFA$.

Functors são morfismos de categorias. O que pode ser entendido com sendo uma categoria de categorias. Apenas tome cuidado com esta metáfora já que podemos incorrer em alguns dos paradoxos da teoria dos conjuntos.

Um Functor mapeias objetos e morfismos de uma categoria em outra, preservando os morfismos originais. Ou, em outras palavras, as composições são preservadas: Se aplicamos o Functor $F$ a composição $h = g\circ f$ teremos $Fh= Fg \circ FF$.

Um exemplo em Haskell pode ser encontrado aqui