在Haskell中,Proxy是一种类型见证值,可以轻松传递某些类型
data Proxy a = Proxy
Run Code Online (Sandbox Code Playgroud)
一个示例用法在json-schema中:
class JSONSchema a where
schema :: Proxy a -> Schema
Run Code Online (Sandbox Code Playgroud)
所以你可以做到schema (Proxy :: Proxy (Int,Char))获得Int-Char-Tuple的JSON表示(可能是一个数组).
为什么人们使用代理?在我看来,同样可以通过以下方式实现
class JSONSchema a where
schema :: Schema a
Run Code Online (Sandbox Code Playgroud)
类似于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`
Run Code Online (Sandbox Code Playgroud)
此外,人们可能会担心这些Proxy值是否在运行时实际根除,这是使用该:: a方法时不存在的优化问题.
如果所:: a采用的方法通过Bounded更短的代码和更少的优化担忧实现相同的结果,为什么人们使用代理?代理有什么好处?
编辑:一些答案和评论者正确地指出,该:: a方法data Schema = ...使用"无用"类型参数污染了类型 - 至少从普通数据结构本身的角度来看,它从未使用过a(参见此处).
建议使用幻像类型Tagged s b,它允许分离两个关注点(Tagged a Schema将非参数模式类型与类型变量组合a),这比:: a方法更好.
所以我的问题应该更好的是代理与标记方法有什么好处?
Chr*_*kle 26
两个例子,一个Proxy是必要的,一个Proxy不会从根本上改变类型,但我倾向于使用它.
Proxy 必要Proxy当你希望消费者能够指定的某种中间类型(未在普通类型签名中公开)时,或者某些等效技巧是必要的.也许中间类型会改变语义,例如read . show :: String -> String.与ScopedTypeVariables启用,我会写
f :: forall proxy a. (Read a, Show a) => proxy a -> String -> String
f _ = (show :: a -> String) . read
Run Code Online (Sandbox Code Playgroud)
> f (Proxy :: Proxy Int) "3"
"3"
> f (Proxy :: Proxy Bool) "3"
"*** Exception: Prelude.read: no parse
Run Code Online (Sandbox Code Playgroud)
proxy参数允许我公开a为类型参数.show . read是一个愚蠢的例子.更好的情况可能是某些算法在内部使用泛型集合,其中所选集合类型具有您希望消费者能够控制的某些性能特征,而无需(或允许)它们提供或接收中间值.
像这样的东西,使用fgl类型,我们不希望暴露内部Data类型.(也许有人可以为这个例子建议一个合适的算法?)
f :: Input -> Output
f = g . h
where
h :: Gr graph Data => Input -> graph Data
g :: Gr graph Data => graph Data -> Output
Run Code Online (Sandbox Code Playgroud)
公开代理参数将允许用户在Patricia树或普通树图实现之间进行选择.
Proxy 作为API或实现方便我有时使用它Proxy作为工具来选择类型类实例,尤其是在递归或归纳类实例中.考虑MightBeA我在这个答案中Either写的关于使用嵌套s的类:
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
Run Code Online (Sandbox Code Playgroud)
这样做是为了提取Maybe Int,比如说,从Either String (Either Bool 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'
Run Code Online (Sandbox Code Playgroud)
在这种情况下,这并不是一个很大的改变; 如果类型签名isA相当复杂,使用代理将是一个很大的改进.
当使用仅用于实现方便时,我通常会导出包装函数,因此用户无需提供代理.
Proxy 与 Tagged在我的所有示例中,type参数a不会添加对输出类型本身有用的任何内容.(在前两个例子中,它与输出类型无关;在最后一个例子中,它是输出类型的冗余.)如果我返回a Tagged a x,消费者总是会立即取消它.此外,用户必须写出完整的类型x,这有时非常不方便,因为它是一些复杂的中间类型.(也许有一天我们可以_在类型签名中使用......)
(我很想听到这个子问题的其他答案;我实际上从来没有写过任何东西Tagged(没有使用短时间重写它Proxy),并想知道我是否遗漏了什么.)
最终,他们将执行相同的功能,您可以在任何一种风格中看到它们.有时候幻象标记你的价值观是合适的,有时你会认为它们是无类型的.
另一种选择是使用Data.Tagged.
class JSONSchema a where
schema :: Tagged a Schema
Run Code Online (Sandbox Code Playgroud)
在这里,我们拥有两全其美的东西,因为它Tagged Schema具有解析实例所需的幻像类型信息,但我们可以轻易地忽略这些信息unTagged :: Tagged s b -> b.
我想说的是,根据这个例子,驱动问题应该是"我想考虑对Schemas 进行打字操作吗?".如果答案是"否",那么你将偏向于Proxy或Tagged接近.如果答案是"是",那么这Schema a是一个很好的解决方案.
最后一点,你可以使用这种Proxy方法(有点hackily)没有任何导入.你有时会看到这种风格
class JSONSchema a where
schema :: proxy a -> Schema
Run Code Online (Sandbox Code Playgroud)
现在,它Proxy已成为一个具有暗示性命名的类型变量,我们只能执行以下操作
foo :: Schema
foo = schema ([] :: [X])
Run Code Online (Sandbox Code Playgroud)
而且根本不需要进口Proxy.我个人认为这是一个完整的黑客工作,虽然这可能会让读者感到困惑.