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 dadosNã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
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)
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 detalhesUm 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 detalhadoLembre-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 #-}
É 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.
O elemento conjugado
Maybe transforma um semigrupo em monóide!
Liberalização de restrições nas versões recentes do GHCDe 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:
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 detalhadaAs 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çãoNo 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))
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))
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?
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!