我们说我有一个内部数据类型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
不导出类型构造函数T
有什么影响?它是否会阻止消费者干涉T a
类型的值?换句话说,这里的导出列表(f, g)
和(f, g, T())
之间是否存在差异?
答案 0 :(得分:4)
消费者将看到的第一件事是该类型没有出现在Haddock文档中。在f
和g
的文档中,类型T
不会像导出类型一样进行超链接。这可能会阻止随意的读者发现T
的类实例。
更重要的是,消费者无法在类型级别使用T
执行任何操作。任何需要编写类型的东西都是不可能的。例如,消费者无法编写涉及T
的新类实例,或者在类型系列中包含T
。 (我不认为有一种方法可以解决这个问题......)
然而,在价值层面,主要限制是消费者cannot write a type annotation including T
:
> :t (f . read) :: Read b => String -> IO (A.T b)
<interactive>:1:39: Not in scope: type constructor or class `A.T'
对类型签名的限制并不像它看起来那么严重。编译器仍然可以推断这样的类型:
> :t f . read
f . read :: Read b => String -> IO (A.T b)
因此,无论类型构造函数T
的可用性如何,都可以表达Haskell的可推断子集中的任何值表达式。如果像我一样,你沉迷于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
> :t unT'
unT' :: (Show b, Read b) => A.T b -> (Int, b)
> x <- f "x"
> unT' x
(-29353, "x")
使用mkT'
实例实施Read
作为练习。
导出像Generic
这样的东西会彻底破坏任何遏制的想法,但你可能会期望这样。
在Haskell的角落里,type signatures are necessary或其中asTypeOf
- 样式的技巧不起作用,我想不会导出类型构造函数实际上可以阻止消费者使用导出列表做一些事情(f, g, T())
。
导出在导出的任何值的类型中使用的所有类型构造函数。在这里,继续在导出列表中包含T()
。除了弄脏文档之外,离开它并没有完成任何事情。如果要公开纯粹抽象的不可变类型,请使用带有隐藏构造函数的newtype
而不使用类实例。