即使你不能对它们进行模式匹配,新类型也不会产生费用吗?

时间:2017-03-13 13:06:16

标签: haskell newtype

上下文

我所知道的大多数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之类的访问器。

TL; DR

即使newtype显示为另一个参数类型的(可能是深埋的)参数,使用newtype 永远不会会产生成本吗?

2 个答案:

答案 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 :: Speedw :: 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构造函数完全参数化。