大功率新型

Newtype是专门的数据类型声明。 这样它只包含一个构造函数和一个字段。

newtype Foo a = Bar a newtype Id = MkId Word 


典型的新手问题


数据类型数据有什么区别?

 data Foo a = Bar a data Id = MkId Word 

新类型的主要特点是它与唯一字段包含相同的部分。 更准确地说,它在类型级别上不同于原始类型,但是在内存中具有相同的表示形式,并且它是经过严格计算的(不是延迟的)。
简而言之-新类型由于其呈现而更加有效。

是的,这对我没有任何意义...我将使用数据
不,好的,最后,您始终可以为严格(而非惰性)字段启用扩展名-funpack-strict-fields :)或直接指定

 data Id = MkId !Word 

尽管如此,新类型的功能不仅限于计算效率。 他们更强大!

3个新角色




隐藏实施


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

newtype与原始的(内部只是Word)不同
但是我们将MkId构造函数隐藏在模块外部。

实施实施


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

尽管在Haskell2010标准中不是这种情况,但是通过扩展通用newTypes的输出,您可以自动推断newtype的行为与内部字段的行为相同。 在我们的案例中, Eq IdNum Id的行为与Eq WordNum Word相同

通过扩展精细育种( DerivingVia )可以实现更多的目标,但以后会更多。

实施选择


尽管有自己的构造函数,但在某些情况下,您可以使用内部表示形式。

挑战赛


有一个整数列表。 仅需一遍即可找到最大和总计金额。
并且不要使用foldlfolds软件包

典型答案


当然, ! :)

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

并且,最终功能的描述如下:

 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) -} 

如果仔细观察,您会在两边看到类似的操作: 只是el`max` mel + s 。 在两种情况下,都是映射和二进制操作。 空元素为Nothing0

是的,这些是类人动物!

Monoid和Semigroup更详细
半群是关联二进制运算的一个属性

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

Monoid是关联运算的属性(即半群)

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

它具有一个空元素,该元素不会更改右侧或左侧的任何元素

 x ⋄ empty == x == empty ⋄ x 


max(+)都是关联的,都具有空元素-Nothing0

而且,monoid映射与卷积的组合是可折叠的

可折叠更详细
回想一下折叠的定义:

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


让我们将折叠行为应用于max(+) 。 我们最多只能组织一个Word monoid实现。 现在该利用新类型选择的实现了!

 {-# 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 

有必要发表评论。

事实是,要成为数据类型Max的单等式,我们需要一个最小元素,即要存在一个空元素。 因此,只有有限的Max a可以是一个半身像。

理论上正确的最大元素等式
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


因此,我们将不得不以某种方式转换数据类型,以便出现一个空元素,并可以使用混凝。

 -- 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) 

共轭Maybe元素将一个半群变成一个半群!

放宽GHC最新版本中的限制
早在GHC 8.2中,就需要一个monoid类型约束

 instance Monoid a => Monoid (Maybe a) 

这意味着我们需要另一种新类型:

 -- 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 

而且在GHC 8.4中已经非常简单了,它只需要半组类型限制,甚至不需要创建Option类型。

 instance Semigroup a => Monoid (Maybe a) 


折叠反应


好了,现在使用可折叠性和箭头更新代码。
我们记得(。)只是一个功能组成:

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

记住fmap是一个函子:

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

并且它对Maybe的实现描述得更高。

箭头更详细
箭头是某些功能的属性,使您可以在框图中使用它们。
可以在这里找到更多详细信息: 箭头:计算的通用接口
在我们的例子中,我们使用箭头功能
那是

 instance Arrow (->) 

我们将使用以下功能:

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

对于我们的情况
 abc == (->) bc == b -> c 

因此,我们功能的签名减少到:

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

或者用简单的话来说,一个函数(***)将具有一个参数(和一个输出类型)的两个函数组合成一个函数,该函数在输入和输出分别具有一对参数,并且具有一对输出类型。

函数(&&&)是简化版本(***) ,其中两个函数的输入参数的类型相同,并且在输入处我们没有一对参数,而是一个参数。

总计,统一功能已获取格式:

 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)) -} 

结果很简短!

但是,包装和反转嵌套类型的数据仍然很麻烦!
您可以进一步减少它,无资源的强制转换将为我们提供帮助!

安全,无资源的强制转换和类型角色


Unsafe.Coerce包中有一个函数-unsafeCoerce

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

该函数强制不安全地将类型从a转换为b
实际上,该函数是魔术,它告诉编译器将类型a的数据视为类型b ,而不考虑此步骤的后果。

它可以用于转换嵌套类型,但是您需要非常小心。

2014年,发生了一场新型革命,即安全,无资源的强制转换!

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

此功能开启了使用newtype的新时代。

强制强制转换器适用于在存储器中具有相同结构的类型。 它看起来像类型类,但实际上GHC在编译时会转换类型,因此无法自行定义实例。
Data.Coerce.coerce函数允许您在没有资源的情况下转换类型,但是为此,我们需要访问类型构造函数。

现在简化我们的功能:

 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) 

我们避免了提取嵌套类型的例程;我们做到这一点时不会浪费资源,而仅使用一个函数。

嵌套数据类型的作用


使用coerce函数我们可以强制转换任何嵌套类型。
但是是否有必要广泛使用此功能?

 -- 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 

从语义上讲,从Sorted(Down a)转换为Sorted a是荒谬的。
但是,您可以尝试:

 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) 

一切都会好起来的,但正确的答案是Just(Down 3)
即,为了消除错误行为,引入了类型角色。

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

让我们现在尝试:

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

好多了!

共有3个角色( 类型角色 ):

  • 代表性的 -等同于如果相同的代表性
  • 标称 -必须具有完全相同的类型
  • 幻像 -与真实内容无关。 等同于任何东西

在大多数情况下,编译器足够聪明,可以揭示类型的作用,但可以提供帮助。

细化派生派生行为


由于DerivingVia语言的扩展,新类型的分发角色得到了改善。

从最近发布的GHC 8.6开始,此新扩展已经出现。

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

如您所见,由于改进了输出方式,因此会自动推断出类型行为。
DerivingVia可以应用于支持Coercible的任何类型, 重要的是-完全不消耗资源!

如果DerivingVia支持泛型泛型和强制转换,则DerivingVia不仅可以应用于新类型,还可以应用于任何同构类型。

结论


Types newtype是一种强大的力量,可以极大地简化和改进代码,消除例程并减少资源消耗。

原文翻译 :新类型的强大力量(石井宏 美)

PS:我认为,在这篇文章发表于一年多以前(不是我的文章)之后, Haskell中关于新类型的新类型的魔力将变得更加清晰!

Source: https://habr.com/ru/post/zh-CN425405/


All Articles