我所知道的大多数Haskell教程(例如LYAH)都将newtypes作为一种免费的习惯用法,它可以强制实现更多类型的安全性。例如,此代码将进行类型检查:
type Speed = Double
type Length = Double
computeTime :: Speed -> Length -> Double
computeTime v l = l / v
但这不会:
newtype Speed = Speed { getSpeed :: Double }
newtype Length = Length { getLength :: Double }
-- wrong!
computeTime :: Speed -> Length -> Double
computeTime v l = l / v
这将:
-- right
computeTime :: Speed -> Length -> Double
computeTime (Speed v) (Length l) = l / v
在这个特定示例中,编译器知道Speed
只是Double
,因此模式匹配没有实际意义,不会生成任何可执行代码。
当newtypes作为参数类型的参数出现时,它们是否仍然没有成本?例如,考虑一个新类型列表:
computeTimes :: [Speed] -> Length -> [Double]
computeTimes vs l = map (\v -> getSpeed v / l) vs
我还可以在lambda中对速度进行模式匹配:
computeTimes' :: [Speed] -> Length -> [Double]
computeTimes' vs l = map (\(Speed v) -> v / l) vs
在任何一种情况下,出于某种原因,我觉得真正的工作正在完成!当newtype被埋没在嵌套的参数数据类型的深层树中时,我开始感到更加不舒服,例如, Map Speed [Set Speed]
;在这种情况下,可能很难或不可能对newtype进行模式匹配,并且必须使用getSpeed
之类的访问器。
即使newtype显示为另一个参数类型的(可能是深埋的)参数,使用newtype 永远不会会产生成本吗?
答案 0 :(得分:6)
他们自己newtypes
是免费的。应用它们的构造函数或模式匹配就没有成本。
当用作其他类型的参数时,例如[T]
如果[T]
是[T']
T`,则T
的表示与newtype for
的表示完全相同。因此,性能没有任何损失。
然而,我可以看到两个主要的警告。
newtype
和实例首先,newtype
经常用于引入类型类的新instance
。显然,当这些是用户定义的时,并不能保证它们具有与原始实例相同的成本。例如,使用时
newtype Op a = Op a
instance Ord a => Ord (Op a) where
compare (Op x) (Op y) = compare y x
比较两个Op Int
的成本略高于比较Int
,因为需要交换参数。 (我在这里忽略了优化,这可能会在触发时使这个成本免费。)
newtypes
用作类型参数第二点更微妙。考虑以下两种身份[Int] -> [Int]
id1, id2 :: [Int] -> [Int]
id1 xs = xs
id2 xs = map (\x->x) xs
第一个有不变的成本。第二个具有线性成本(假设没有优化触发器)。一个聪明的程序员应该更喜欢第一个实现,它也更容易编写。
假设现在我们只在参数类型上引入newtypes
:
id1, id2 :: [Op Int] -> [Int]
id1 xs = xs -- error!
id2 xs = map (\(Op x)->x) xs
由于类型错误,我们无法再使用常量成本实现。线性成本实施仍然有效,并且是唯一的选择。
现在,这非常糟糕。 [Op Int]
的输入表示与[Int]
完全相同。然而,类型系统禁止我们以有效的方式执行身份!
要解决此问题,请在Haskell中引入safe coercions。
id3 :: [Op Int] -> [Int]
id3 = coerce
在某些假设下,神奇coerce
函数会根据需要删除或插入newtype
以进行类型匹配,甚至 in 其他类型,如{{1} } 以上。此外,它是一个零成本函数。
请注意[Op Int]
仅在某些条件下有效(编译器会检查它们)。其中之一是coerce
构造函数必须可见:如果模块未导出newtype
,则无法将Op :: a -> Op a
强制转换为Op Int
,反之亦然。实际上,如果模块导出类型但不导出构造函数,那么通过Int
无论如何都可以访问构造函数。这使得"智能构造函数"习语仍然安全:模块仍然可以通过不透明类型强制执行复杂的不变量。
答案 1 :(得分:3)
一个(完全)参数类型堆栈中新类型的深度埋藏并不重要。在运行时,值v :: Speed
和w :: Double
完全无法区分 - 编译器会删除包装器,因此即使v
实际上只是指向单个64位浮点数的指针在记忆中。该指针是存储在列表还是树中,或者是否存在差异。 getSpeed
是一个无操作,根本不会以任何方式出现在运行时。
那么“完全参数化”是什么意思?问题是,newtypes 可以显然在编译时通过类型系统产生影响。特别是,它们可以指导实例解析,因此调用不同类方法的新类型肯定会比包装类型更差(或者,更容易,更好!)性能。例如,
class Integral n => Fibonacci n where
fib :: n -> Integer
instance Fibonacci Int where
fib = (fibs !!)
where fibs = [ if i<2 then 1
else fib (i-2) + fib (i-1)
| i<-[0::Int ..] ]
这个实现非常慢,因为它使用一个惰性列表(并在其中反复执行查找)进行记忆。另一方面,
import qualified Data.Vector as Arr
-- | A number between 0 and 753
newtype SmallInt = SmallInt { getSmallInt :: Int }
instance Fibonacci SmallInt where
fib = (fibs Arr.!) . getSmallInt
where fibs = Arr.generate 754 $
\i -> if i<2 then 1
else fib (SmallInt $ i-2) + fib (SmallInt $ i-1)
这个fib
要快得多,因为输入限制在一个小范围内,严格分配所有结果并将它们存储在快速 O (1)查找数组,不需要脊椎懒惰。
当然,无论您存储数字的结构如何,这都会再次适用。但不同的性能只会因为调用不同的方法实例而产生 - 在运行时这意味着简单,完全不同的函数。
现在,完全参数化类型构造函数必须能够存储任何类型的值。特别是,它不能对包含的数据施加任何类限制,因此也不会调用任何类方法。因此,如果您只是处理通用[a]
列表或Map Int a
地图,则无法实现这种性能差异。但是,当您处理GADT时,可能会发生这种情况。在这种情况下,即使实际的内存布局也可能完全不同,例如
{-# LANGUAGE GADTs #-}
import qualified Data.Vector as Arr
import qualified Data.Vector.Unboxed as UArr
data Array a where
BoxedArray :: Arr.Vector a -> Array a
UnboxArray :: UArr.Unbox a => UArr.Vector a -> Array a
可能允许您比Double
值更有效地存储Speed
值,因为前者可以存储在缓存优化的未装箱数组中。这是唯一可能的,因为UnboxArray
构造函数不完全参数化。