假设我有一个内部数据类型,T a用于导出函数的签名:
module A (f, g) where
newtype T a = MkT { unT :: (Int, a) }
deriving (Functor, Show, Read) -- for internal use
f :: a -> IO (T a)
f a = fmap (\i -> T (i, a)) randomIO
g :: T a -> a
g = snd . unT
Run Code Online (Sandbox Code Playgroud)
不导出类型构造函数有T什么影响?它是否会阻止消费者干涉类型的值T a?换句话说,出口清单(f, g)和(f, g, T())这里有区别吗?
消费者首先看到的是该类型没有出现在 Haddock 文档中。f在和的文档中g,该类型T不会像导出类型那样具有超链接。这可能会阻止临时读者发现 的T类实例。
更重要的是,消费者不能T在类型级别上做任何事情。任何需要编写类型的事情都是不可能的。例如,使用者不能编写涉及T或包含T在类型族中的新类实例。(我认为没有办法解决这个问题......)
然而,在值级别,主要限制是消费者无法编写类型注释,包括T:
> :t (f . read) :: Read b => String -> IO (A.T b)
<interactive>:1:39: Not in scope: type constructor or class `A.T'
Run Code Online (Sandbox Code Playgroud)
对类型签名的限制并不像看起来那么重要。编译器仍然可以推断出这样的类型:
> :t f . read
f . read :: Read b => String -> IO (A.T b)
Run Code Online (Sandbox Code Playgroud)
因此,无论类型构造函数是否可用,都可以表达 Haskell 可推断子集中的任何值表达式T。如果您像我一样沉迷于ScopedTypeVariables大量注释,那么您可能会对下面的定义感到有点惊讶unT'。
此外,由于类型类实例具有全局范围,因此使用者可以使用任何可用的类函数而没有额外的限制。根据所涉及的类,这可能允许对未公开类型的值进行重大操作。对于像这样的类Functor,消费者还可以自由地操作类型参数,因为有一个可用的类型函数T a -> T b。
在 的示例中T,派生Show当然会暴露“内部” Int,并为消费者提供足够的信息来黑客实现unT:
-- :: (Show a, Read a) => T a -> (Int, a)
unT' = (read . strip . show') `asTypeOf` (mkPair . g)
where
strip = reverse . drop 1 . reverse . drop 9
-- :: T a -> String
show' = show `asTypeOf` (mkString . g)
mkPair :: t -> (Int, t)
mkPair = undefined
mkString :: t -> String
mkString = undefined
Run Code Online (Sandbox Code Playgroud)
> :t unT'
unT' :: (Show b, Read b) => A.T b -> (Int, b)
> x <- f "x"
> unT' x
(-29353, "x")
Run Code Online (Sandbox Code Playgroud)
mkT'使用实例实现Read留作练习。
推导出类似的东西Generic将完全颠覆任何遏制的想法,但你可能会预料到这一点。
在 Haskell 的角落里,类型签名是必要的,或者asTypeOf-style 技巧不起作用的地方,我想不导出类型构造函数实际上可能会阻止消费者使用导出列表做一些他们可以做的事情(f, g, T())。
导出在导出的任何值的类型中使用的所有类型构造函数。在这里,继续并将其包含T()在您的导出列表中。除了使文档变得混乱之外,忽略它不会产生任何其他效果。如果要公开纯粹抽象的不可变类型,请使用newtype带有隐藏构造函数且没有类实例的 a 。