Haskell:为什么要使用Proxy?

nh2*_*nh2 39 haskell

在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.这里使用代理有两个原因:

首先,它消除了消费者的类型签名.您可以拨打isAisA (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相当复杂,使用代理将是一个很大的改进.

当使用仅用于实现方便时,我通常会导出包装函数,因此用户无需提供代理.

ProxyTagged

在我的所有示例中,type参数a不会添加对输出类型本身有用的任何内容.(在前两个例子中,它与输出类型无关;在最后一个例子中,它是输出类型的冗余.)如果我返回a Tagged a x,消费者总是会立即取消它.此外,用户必须写出完整的类型x,这有时非常不方便,因为它是一些复杂的中间类型.(也许有一天我们可以_在类型签名中使用......)

(我很想听到这个子问题的其他答案;我实际上从来没有写过任何东西Tagged(没有使用短时间重写它Proxy),并想知道我是否遗漏了什么.)

  • `PartialTypeSignatures` 来了!确实,`Tagged` 比以前少了很多痛苦。 (2认同)

J. *_*son 9

最终,他们将执行相同的功能,您可以在任何一种风格中看到它们.有时候幻象标记你的价值观是合适的,有时你会认为它们是无类型的.

另一种选择是使用Data.Tagged.

class JSONSchema a where
  schema :: Tagged a Schema
Run Code Online (Sandbox Code Playgroud)

在这里,我们拥有两全其美的东西,因为它Tagged Schema具有解析实例所需的幻像类型信息,但我们可以轻易地忽略这些信息unTagged :: Tagged s b -> b.

我想说的是,根据这个例子,驱动问题应该是"我想考虑对Schemas 进行打字操作吗?".如果答案是"否",那么你将偏向于ProxyTagged接近.如果答案是"是",那么这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.我个人认为这是一个完整的黑客工作,虽然这可能会让读者感到困惑.

  • 我更喜欢`proxy`到`Proxy`,因为我经常可以访问可以与`proxy a`统一的东西,比如`Nothing`或`[]`,可以直接用作参数而不是创建一个新的`Proxy :: Proxy T`,我认为它过于冗长. (3认同)
  • 当 `Proxy` 不在 `base` 中时,`proxy`“黑客工作”可能更相关。特别是因为`tagged` 包实际上经历了很多版本。但是如果你的目标是 `base>=4.7`,那么使用具体类型更有意义,这样读者就可以点击 Haddock 链接来理解它。(我记得我自己有一段时间觉得这很令人困惑。) (2认同)