在 Haskell 中, Proxy是一个类型见证值,可以轻松传递某些类型
data Proxy a = Proxy
json-schema 中有一个使用示例:
class JSONSchema a where
schema :: Proxy a -> Schema
因此您可以执行 schema (Proxy::Proxy (Int,Char))
来获取 Int-Char-Tuple 的 JSON 表示形式(可能是一个数组)。
人们为什么使用代理?在我看来,同样可以通过以下方式实现
class JSONSchema a where
schema :: Schema a
与Bounded
类型类的工作方式类似。
我首先认为使用代理时获取某些给定值的模式可能更容易,但事实似乎并非如此:
{-# LANGUAGE ScopedTypeVariables #-}
schemaOf :: JSONSchema a => a -> Schema a
schemaOf (v :: x) = schema (Proxy :: Proxy x) -- With proxy
schemaOf (v :: x) = schema :: Schema x -- With `:: a`
schemaOf _ = schema -- Even simpler with `:: a`
此外,人们可能会担心 Proxy
值是否实际上在运行时被消除,这是一个优化问题,而使用 ::a
方法时则不存在.
如果Bounded
采用的::a
方法可以用更短的代码实现相同的结果,并且不用担心优化,那么为什么人们还要使用代理呢? 代理有什么好处?
编辑:一些答案和评论者正确地指出 ::a
方法用 a 污染了 data Schema = ...
类型“无用”类型参数 - 至少从普通数据结构本身的角度来看,它从不使用 a
( see here )。
建议使用幻像类型Tagged s b
相反,它允许分离两个关注点(Tagged a Schema
将非参数模式类型与类型变量 a
结合起来),这比 ::一种
方法。
所以我的问题应该是代理与标记方法相比有什么好处?
最佳答案
两个示例,一个需要 Proxy
,另一个示例 Proxy
不会从根本上改变类型,但我还是倾向于使用它。
代理
必要
当您希望使用者能够指定某些未在正常类型签名中公开的中间类型时,Proxy
或一些等效的技巧是必要的。也许中间类型改变了语义,例如 read 。显示::字符串->字符串
。启用 ScopedTypeVariables
后,我会写
f :: forall proxy a. (Read a, Show a) => proxy a -> String -> String
f _ = (show :: a -> String) . read
> f (Proxy :: Proxy Int) "3"
"3"
> f (Proxy :: Proxy Bool) "3"
"*** Exception: Prelude.read: no parse
代理参数允许我将a
公开为类型参数。 显示 . read
是一个愚蠢的例子。更好的情况可能是某些算法在内部使用通用集合,其中所选的集合类型具有一些您希望消费者能够控制的性能特征,而不需要(或允许)他们提供或接收中间值。
像这样,使用 fgl
类型,我们不想想要公开内部Data
类型。 (也许有人可以为这个例子建议一个合适的算法?)
f :: Input -> Output
f = g . h
where
h :: Gr graph Data => Input -> graph Data
g :: Gr graph Data => graph Data -> Output
公开代理参数将允许用户在 Patricia 树或普通 TreeMap 实现之间进行选择。
代理
作为 API 或实现便利
我有时使用Proxy
作为选择类型类实例的工具,特别是在递归或归纳类实例中。考虑我在 this answer about using nested Either
s 中编写的 MightBeA
类:
class MightBeA t a where
isA :: proxy t -> a -> Maybe t
fromA :: t -> a
instance MightBeA t t where
isA _ = Just
fromA = id
instance MightBeA t (Either t b) where
isA _ (Left i) = Just i
isA _ _ = Nothing
fromA = Left
instance MightBeA t b => MightBeA t (Either a b) where
isA p (Right xs) = isA p xs
isA _ _ = Nothing
fromA = Right . fromA
这个想法是从Either String (Either Bool Int)
中提取一个Maybe Int
。 isA
的类型基本上是 a -> Maybe t
。这里使用代理有两个原因:
首先,它消除了消费者的类型签名。您可以将 isA
调用为 isA (Proxy::Proxy Int)
,而不是 isA::MightBeA Int a => a -> Maybe Int
。
其次,通过传递代理,我可以更轻松地思考归纳案例。使用 ScopedTypeVariables,可以在没有代理参数的情况下重写该类;归纳情况将实现为
instance MightBeA' t b => MightBeA' t (Either a b) where
-- no proxy argument
isA' (Right xs) = (isA' :: b -> Maybe t) xs
isA' _ = Nothing
fromA' = Right . fromA'
在这种情况下,这并不是一个很大的变化;如果 isA
的类型签名要复杂得多,那么使用代理将会是一个很大的改进。
当专门为了实现方便而使用时,我通常会导出一个包装函数,这样用户就不需要提供代理。
代理
与标记
在我的所有示例中,类型参数 a
不会为输出类型本身添加任何有用的内容。 (在前两个示例中,它与输出类型无关;在最后一个示例中,它是输出类型的冗余。)如果我返回一个Tagged a x
,则消费者总是会立即取消它的标记。此外,用户必须完整地写出x
的类型,这有时非常不方便,因为它是一些复杂的中间类型。 (也许someday we'll be able to use _
in type signatures...)
(我有兴趣听到关于这个子问题的其他答案;我实际上从未使用 Tagged
编写过任何内容(没有使用 Proxy
快速重写它) )并想知道我是否遗漏了一些东西。)
关于Haskell:为什么使用代理?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27044209/