Newtypes de grande potência

Newtype é uma declaração de tipo de dados especializada. De modo que ele contenha apenas um construtor e campo.

newtype Foo a = Bar a newtype Id = MkId Word 


Perguntas típicas para iniciantes


Qual é a diferença dos dados do tipo de dados?

 data Foo a = Bar a data Id = MkId Word 

A principal especificidade do newtype é que ele consiste nas mesmas partes que seu único campo. Mais precisamente, difere do original no nível de tipo, mas tem a mesma representação na memória e é calculada estritamente (não preguiçosamente).
Em resumo - o newtype é mais eficaz devido à sua apresentação.

Sim, isso não significa nada para mim ... vou usar dados
Não, bem, no final, você sempre pode ativar a extensão -funpack-strict-fields :) para campos estritos (não preguiçosos) ou especificar diretamente

 data Id = MkId !Word 

Ainda assim, o poder do novo tipo não se limita à eficiência computacional. Eles são muito mais fortes!

3 funções de tipo novo




Ocultando a implementação


 module Data.Id (Id()) where newtype Id = MkId Word 

newtype difere do original, apenas internamente o Word .
Mas ocultamos o construtor MkId fora do módulo.

Implementação Implementação


 {-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq) 

Embora isso não esteja no padrão Haskell2010, expandindo a saída de newTypes genéricos, você pode inferir automaticamente o comportamento de newtype da mesma forma que o comportamento do campo interno. No nosso caso, o comportamento de Eq Id e Num Id é o mesmo que Eq Word e Num Word .

Muito mais pode ser alcançado através da expansão da criação refinada ( DerivingVia ), mas mais sobre isso mais tarde.

Implementação de escolha


Apesar de seu próprio construtor, em alguns casos você pode usar sua representação interna.

Desafio


Há uma lista de números inteiros. Encontre o valor máximo e total em apenas um passe na lista.
E não use pacotes foldl e folds .

Resposta típica


Claro, dobre ! :)

 foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> b -} 

E, a função final é descrita assim:

 aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = foldr (\el (m, s) -> (Just el `max` m, el + s)) (Nothing, 0) {- ghci> aggregate [1, 2, 3, 4] (Just 4, 10) -} 

Se você observar atentamente, poderá ver operações semelhantes nos dois lados: Apenas el `max` me el + s . Nos dois casos, mapeamento e operação binária. E os elementos vazios são Nothing e 0 .

Sim, estes são monoides!

Monóide e Semigrupo em mais detalhes
Um semigrupo é uma propriedade de uma operação binária associativa

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

Um monóide é uma propriedade de uma operação associativa (ou seja, um semigrupo)

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

que possui um elemento vazio que não altera nenhum elemento à direita ou à esquerda

 x ⋄ empty == x == empty ⋄ x 


Ambos max e (+) são associativos, ambos possuem elementos vazios - Nothing e 0 .

E a combinação de mapeamento de monoides junto com a convolução é dobrável !

Dobrável mais detalhado
Lembre-se da definição de dobrar:

 class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ... 


Vamos aplicar o comportamento de dobrar para max e (+) . Não podemos organizar mais que uma implementação do Word monóide. Está na hora de tirar proveito da implementação da opção newtype !

 {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- already in Data.Semigroup & Data.Monoid newtype Sum a = Sum {getSum :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Sum a) where (<>) = (+) instance (Num a, Ord a) => Monoid (Sum a) where mempty = Sum 0 newtype Max a = Max {getMax :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Max a) where (<>) = max 

É necessário fazer uma observação.

O fato é que, para ser um monóide para o tipo de dados Max , precisamos de um elemento mínimo, ou seja, para que exista um elemento vazio. Portanto, apenas um Max a limitado pode ser um monóide.

Teoricamente correto, elemento máximo monóide
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


Então, de alguma forma, teremos que converter nosso tipo de dados para que um elemento vazio apareça e possamos usar a coagulação.

 -- already in Prelude data Maybe a = Nothing | Just a instance Semigroup a => Semigroup (Maybe a) where Nothing <> b = b b <> Nothing = b (Just a) <> (Just b) = Just (a <> b) instance Semigroup a => Monoid (Maybe a) where mempty = Nothing -- ------ instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just b) = Just (fb) 

O elemento conjugado Maybe transforma um semigrupo em monóide!

Liberalização de restrições nas versões recentes do GHC
De volta ao GHC 8.2, era necessário um monóide na restrição de tipo

 instance Monoid a => Monoid (Maybe a) 

o que significa que precisávamos de outro tipo:

 -- already in Data.Semigroup & Data.Monoid newtype Option a = Option {getOption :: Maybe a} deriving (Eq, Ord, Semigroup) instance (Ord a, Semigroup a) => Monoid (Option a) where mempty = Option Nothing 

E já é muito mais simples no GHC 8.4, onde apenas um semigrupo com restrição de tipo é necessário, e mesmo não há necessidade de criar um tipo de opção.

 instance Semigroup a => Monoid (Maybe a) 


Resposta dobrável


Bem, agora atualize o código usando recolhibilidade e setas.
Lembramos que (.) É apenas uma composição funcional:

  (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx) 

E lembre-se de que o fmap é um functor:

 fmap :: Functor f => (a -> b) -> fa -> fb 

e sua implementação para Maybe é descrita um pouco mais.

Seta mais detalhada
As setas são propriedades de algumas funções que permitem trabalhar com elas em um diagrama de blocos.
Mais detalhes podem ser encontrados aqui: Setas: uma interface geral para computação
No nosso caso, usamos a função Arrows
Isso é

 instance Arrow (->) 

Vamos usar as funções:

 (***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c') 

Para o nosso caso
 abc == (->) bc == b -> c 

E, consequentemente, a assinatura de nossas funções é reduzida para:

 (***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c')) 

Ou, em palavras bastante simples, a função (***) combina duas funções com um argumento (e um tipo de saída) em uma função com o trabalho de um par de argumentos na entrada e na saída, respectivamente, um par de tipos de saída.

A função (&&&) é uma versão simplificada (***) , em que o tipo dos argumentos de entrada das duas funções é o mesmo, e na entrada não temos um par de argumentos, mas um argumento.

Total, a função unificadora adquiriu a forma:

 import Data.Semigroup import Data.Monoid import Control.Arrow aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = (fmap getMax *** getSum) . (foldMap (Just . Max &&& Sum)) {- -- for GHC 8.2 aggregate = (fmap getMax . getOption *** getSum) . (foldMap (Option . Just . Max &&& Sum)) -} 

Acabou muito brevemente!

Mas, ainda é cansativo quebrar e inverter dados de tipos aninhados!
Você pode reduzi-lo ainda mais, e uma conversão forçada sem recursos nos ajudará!

Conversão forçada segura e sem recursos e funções de tipo


Existe uma função do pacote Unsafe.Coerce - unsafeCoerce

 import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b 

A função insegura à força converte o tipo: de a para b .
De fato, a função é mágica, diz ao compilador para considerar os dados do tipo a como tipo b , sem levar em consideração as conseqüências desta etapa.

Pode ser usado para converter tipos aninhados, mas você precisa ter muito cuidado.

Em 2014, houve uma revolução com o newtype , a saber, uma conversão forçada, segura e sem recursos!

 import Data.Coerce(coerce) coerce :: Coercible ab => a -> b 

Esse recurso abriu uma nova era no trabalho com newtype .

O Coercible Forced Converter funciona com tipos que têm a mesma estrutura na memória. Parece uma classe de tipo, mas, na verdade, o GHC converte tipos em tempo de compilação e não é possível definir instâncias por conta própria.
A função Data.Coerce.coerce permite converter tipos sem recursos, mas para isso precisamos ter acesso aos construtores de tipos.

Agora simplifique nossa função:

 import Data.Semigroup import Data.Monoid import Control.Arrow import Data.Coerce aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = coerce . (foldMap (Just . Max &&& Sum)) -- coerce :: (Maybe (Max Integer), Sum Integer) -> (Maybe Integer, Integer) 

Evitamos a rotina de puxar tipos aninhados; fizemos isso sem desperdiçar recursos com apenas uma função.

Funções dos tipos de dados aninhados


Com a função coagir, podemos forçar a conversão de qualquer tipo aninhado.
Mas é necessário usar esse recurso tão amplamente?

 -- already in Data.Ord -- Down a - reversed order newtype Down a = Down a deriving (Eq, Show) instance Ord a => Ord (Down a) where compare (Down x) (Down y) = y `compare` x import Data.List(sort) -- Sorted data Sorted a = Sorted [a] deriving (Show, Eq, Ord) fromList2Sorted :: Ord a => [a] -> Sorted a fromList2Sorted = Sorted . sort -- minimum: O(1) ! minView :: Sorted a -> Maybe a minView (Sorted []) = Nothing minView (Sorted (a : _)) = Just a 

Semanticamente, é um absurdo converter para Sorted a de Sorted (Abaixo a) .
No entanto, você pode tentar:

 ghci> let h = fromList2Sorted [1,2,3] :: Sorted Int ghci> let hDown = fromList2Sorted $ fmap Down [1,2,3] :: Sorted (Down Int) ghci> minView h Just (Down 1) ghci> minView (coerce h :: Sorted (Down Int)) Just (Down 1) ghci> minView hDown Just (Down 3) 

Tudo ficaria bem, mas a resposta correta é Just (Down 3) .
Ou seja, para eliminar o comportamento incorreto, as funções de tipo foram introduzidas.

 {-# LANGUAGE RoleAnnotations #-} type role Sorted nominal 

Vamos tentar agora:

 ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce' 

Significativamente melhor!

No total, existem 3 papéis ( tipo papel ):

  • representacional - equivalente se a mesma representação
  • nominal - deve ter exatamente o mesmo tipo
  • fantasma - independente do conteúdo real. Equivalente a qualquer coisa

Na maioria dos casos, o compilador é inteligente o suficiente para revelar o papel do tipo, mas pode ser ajudado.

Comportamento de derivação refinada


Graças à expansão da linguagem DerivingVia , a função de distribuição do newtype melhorou.

A partir do GHC 8.6, lançado recentemente, essa nova extensão apareceu.

 {-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word 

Como você pode ver, o comportamento do tipo é inferido automaticamente devido ao refinamento de como produzir.
O DerivingVia pode ser aplicado a qualquer tipo que ofereça suporte ao Coercible e o mais importante - completamente sem consumo de recursos!

Ainda mais, o DerivingVia pode ser aplicado não apenas ao newtype , mas também a qualquer tipo isomórfico, se eles suportarem genéricos genéricos e conversão forçada coercível .

Conclusões


Tipos newtype é uma força poderosa que simplifica e aprimora muito o código, elimina a rotina e reduz o consumo de recursos.

Tradução original : O grande poder dos novos tipos (Hiromi Ishii)

PS Acho que, depois deste artigo, publicado há mais de um ano [não pelo meu] artigo, a magia do newtype em Haskell sobre novos tipos se tornará um pouco mais clara!

Source: https://habr.com/ru/post/pt425405/


All Articles