Back-End

18 nov, 2014

Um bom truque para famílias de tipos parcialmente fechados

Publicidade

Este artigo fala de um truque bastante interessante sobre famílias de tipos fechados, em Haskell. Famílias de tipos normais são “abertas” porque qualquer arquivo pode ser adicionado em novas instâncias do tipo. Famílias de tipos fechados, porém, devem ser definidas em um único arquivo. Isso permite que o checador do tipo chegue a mais hipóteses, e então as famílias de tipos fechadas são mais poderosas. Neste texto, falaremos dessa restrição e definiremos certas famílias de tipos fechados em alguns arquivos.

Precisamos de apenas duas extensões de linguagem para essa técnica:

> {-# LANGUAGE TypeFamilies #-}
> {-# LANGUAGE UndecidableInstances #-}

Mas, para o nosso exemplo, usaremos também essas extensões em algumas importações básicas:

> {-# LANGUAGE KindSignatures #-}
> {-# LANGUAGE MultiParamTypeClasses #-}
> {-# LANGUAGE ConstraintKinds #-}

> import Data.Proxy
> import GHC.Exts

Vamos começar.

Considere as classes:

> class Param_a (p :: * -> Constraint) t
> class Param_b (p :: * -> Constraint) t
> class Param_c (p :: * -> Constraint) t
> class Base t

Essas classes podem ser encadeadas juntos, como:

> type Telescope_abc = Param_a (Param_b (Param_c Base))

É fácil escrever uma família de tipos que retorne o “head” dessa lista. Em uma luneta (telescope), as lentes mais próximas de você são chamadas de “eye piece” (ocular), então é assim que chamamos nossa família de tipos:

> type family EyePiece ( p :: * -> Constraint ) :: * -> Constraint

> type instance EyePiece (Param_a p) = Param_a Base
> type instance EyePiece (Param_b p) = Param_b Base
> type instance EyePiece (Param_c p) = Param_c Base

Novamente, essa família é “aberta” porque novas instâncias podem ser definidas em qualquer arquivo.

Podemos usar esse tipo EyePiece como:

ghci> :t Proxy :: Proxy (EyePiece Telescope_abc)
  :: Proxy (Param_a Base)

Vamos agora tentar escrever uma classe tipo que faça o oposto. Em vez de extrair o primeiro elemento da cadeia, vamos extrair o último. Em uma luneta (telescope), as lentes mais afastadas de você são chamadas de “objective” (objetiva), e vamos chamar nossa família tipo (?)assim. Também precisaremos defini-la como uma família de tipos fechados:

type family Objective (lens :: * -> Constraint) :: * -> Constraint where
  Objective (Param_a p) = Objective p
  Objective (Param_b p) = Objective p
  Objective (Param_c p) = Objective p
  Objective (Param_a Base) = Param_a Base
  Objective (Param_b Base) = Param_b Base
  Objective (Param_c Base) = Param_c Base

Podemos usar a família Objective assim:

ghci> :t Proxy :: Proxy (Objective Telescope_abc)
  :: Proxy (Param_c Base)

A família Objective deve ser fechada. Isso porque a única forma de identificar quando estamos ao final da luneta (telescope) é checando se o parâmetro p é a classe Base. Se for, então está pronto. Se não, devemos continuar descendo o telescope recursivamente. Sem uma família de tipos fechados seria preciso listar explicitamente todos os caminhos recursivos. Isso significa que o tipo O(n2) instancia a qualquer hora que adicionarmos uma nova classe Param_xxx. Isso é ruim e tende ao erro.

Novamente, o lado ruim das famílias de tipos fechados é que elas precisam ser definidas todas em um único lugar. Podemos contornar essa limitação fazendo um “factoring” da família de tipos fechados em uma coleção de famílias de tipos fechados e abertos. Veja o exemplo abaixo:

> type family Objective (lens :: * -> Constraint) :: * -> Constraint
> type instance Objective (Param_a p) = Objective_Param_a (Param_a p)
> type instance Objective (Param_b p) = Objective_Param_b (Param_b p)
> type instance Objective (Param_c p) = Objective_Param_c (Param_c p)
> type instance Objective Base = Base

> type family Objective_Param_a (lens :: * -> Constraint) :: * -> Constraint where
>   Objective_Param_a (Param_a Base) = Param_a Base
>   Objective_Param_a (Param_a p) = Objective p

> type family Objective_Param_b (lens :: * -> Constraint) :: * -> Constraint where
>   Objective_Param_b (Param_b Base) = Param_b Base
>   Objective_Param_b (Param_b p) = Objective p

> type family Objective_Param_c (lens :: * -> Constraint) :: * -> Constraint where
>   Objective_Param_c (Param_c Base) = Param_c Base
>   Objective_Param_c (Param_c p) = Objective p

ghci> :t Proxy :: Proxy (Objective Telescope_abc)
  :: Proxy (Param_c Base)

Com esse factoring, é possível definir a instância Objective para cada Param_xxx em arquivos separados e reter os benefícios das famílias de tipos fechados.

Aqui um outro exemplo. A família RemoveObjective age como uma função init do Prelude:

> type family RemoveObjective (lens :: * -> Constraint) :: * -> Constraint
> type instance RemoveObjective (Param_a p) = RemoveObjective_Param_a (Param_a p)
> type instance RemoveObjective (Param_b p) = RemoveObjective_Param_b (Param_b p)
> type instance RemoveObjective (Param_c p) = RemoveObjective_Param_c (Param_c p)

> type family RemoveObjective_Param_a (lens :: * -> Constraint) :: * -> Constraint where
>   RemoveObjective_Param_a (Param_a Base) = Base
>   RemoveObjective_Param_a (Param_a p) = Param_a (RemoveObjective p)

> type family RemoveObjective_Param_b (lens :: * -> Constraint) :: * -> Constraint where
>   RemoveObjective_Param_b (Param_b Base) = Base
>   RemoveObjective_Param_b (Param_b p) = Param_b (RemoveObjective p)

> type family RemoveObjective_Param_c (lens :: * -> Constraint) :: * -> Constraint where
>   RemoveObjective_Param_c (Param_c Base) = Base
>   RemoveObjective_Param_c (Param_c p) = Param_b (RemoveObjective p)

ghci> :t Proxy :: Proxy (RemoveObjective Telescope_abc)
  :: Proxy (Param_a (Param_b Base))

Claro, você não pode usar isso tem todas as famílias de tipos fechados. Por exemplo, a família RemoveObjective_Param_c acima não pode ser fatorada em nada menor. Mas se você se pegar querendo os benefícios de ambas famílias de tipos fechados e abertos, então o seu tipo provavelmente tem a estrutura.

***

Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em https://izbicki.me/blog/a-neat-trick-for-partially-closed-type-families.html