【问题标题】:Haskell: Why use Proxy?Haskell:为什么要使用代理?
【发布时间】:2014-11-20 16:10:05
【问题描述】:

在 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 方法用“无用”类型参数污染了data Schema = ... 类型——至少从普通数据结构本身的角度来看是这样,它从不使用a (see here)。

建议改用幻象类型Tagged s b,这样可以将两个关注点分开(Tagged a Schema 将非参数模式类型与类型变量a 结合在一起),这比@ 987654342@方法。

所以我的问题最好是代理与标记方法有什么好处?

【问题讨论】:

  • 在这种情况下有一个选择,但您想要代理的示例是typeRep :: Proxy a -> TypeRep。另一种方法是使用旧的方法,typfeOf :: a -> Typeable。但后者令人不快,因为您经常必须将其与typeOf (undefined :: T) 一起使用,恕我直言,最好使用typeRep (Proxy :: Proxy T)
  • 我看不到与Bounded 的类比。使用 hackage.haskell.org/package/tagged-0.7.3/docs/Data-Tagged.html 提供的类型标签是使用代理的有效替代方法。这在某种程度上取决于您是否想要结果类型的额外类型参数。
  • @kosmikus 你是对的,使用Tagged 将是不污染我的Schema 和无用的a 的更清洁方法(因为对于Schemaa 实际上永远不会用于数据声明的左侧)。我将在问题的末尾添加一点,说一个更好的问题是关于ProxyTagged 之间的区别。
  • @augustss 我还不明白为什么typeRep 你需要代​​理。你能详细说明一下吗?
  • 关于typeRep:通过表示的类型参数化TypeRep会破坏它的有用性。如果没有代理,typeRep 的类型将是 Typeable a => TypeRep

标签: haskell


【解决方案1】:

两个例子,一个是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

 

> f (Proxy :: Proxy Int) "3"
"3"
> f (Proxy :: Proxy Bool) "3"
"*** Exception: Prelude.read: no parse

代理参数允许我将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

公开代理参数将允许用户在 Patricia 树或普通树图实现之间进行选择。

Proxy 作为 API 或实现方便

我有时使用Proxy 作为选择类型类实例的工具,尤其是在递归或归纳类实例中。考虑一下我在this answer about using nested Eithers 中写的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 IntisA 的类型基本上是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 的类型签名要复杂得多,那么使用代理将是一个很大的改进。

当专门用于实现方便时,我通常会导出一个包装函数,这样用户就不需要提供代理了。

ProxyTagged

在我的所有示例中,类型参数a 并没有为输出类型本身添加任何有用的东西。 (在前两个示例中,它与输出类型无关;在最后一个示例中,它是输出类型的冗余。)如果我返回Tagged a x,消费者总是会立即取消标记它。此外,用户必须完整地写出x的类型,这有时很不方便,因为它是一些复杂的中间类型。 (可能是someday we'll be able to use _ in type signatures...

(我很想听听关于这个子问题的其他答案;我实际上从来没有使用Tagged 写过任何东西(没有使用Proxy 在短时间内重写它)并且想知道我是否遗漏了一些东西。 )

【讨论】:

  • 两个很好的例子,我没想到这些用例。
  • PartialTypeSignatures 来了! Tagged 确实比以前少了很多痛苦。
【解决方案2】:

最终,它们将执行相同的功能,并且您会以任何一种风格看到它们。有时对您的值进行虚拟标记是合适的,有时您希望将它们视为未键入的。

另一种选择是使用Data.Tagged

class JSONSchema a where
  schema :: Tagged a Schema

这里我们有两全其美的东西,因为 Tagged Schema 具有解析实例所需的幻像类型信息,但我们可以使用 unTagged :: Tagged s b -> b 忽略该信息。

我想说,这个例子中的驱动问题应该是“我想考虑在Schemas 上进行类型化操作吗?”。如果答案是“否”,那么您将偏向于ProxyTagged 方法。如果答案是“是”,那么Schema a 是一个很好的解决方案。

作为最后一点,您可以使用Proxy 方法(有点骇人听闻)而无需任何导入。您有时会在样式中看到这一点

class JSONSchema a where
  schema :: proxy a -> Schema

现在Proxy 已成为一个暗示性命名的类型变量,只有我们可以执行以下操作

foo :: Schema
foo = schema ([] :: [X])

根本不需要导入Proxy。我个人认为这是一个完整的 hack 工作,但最终可能会让读者感到困惑。

【讨论】:

  • Proxy 不在base 中时,proxy“黑客工作”可能更相关。特别是因为tagged 包实际上经历了很多版本。但是,如果您的目标是base>=4.7,那么使用具体类型更有意义,这样读者可以通过 Haddock 链接来理解它。 (我记得自己曾一度感到困惑。)
  • 您的回答很有帮助,但我认为它还不能解释核心观点。既然提到了Tagged,我注意到我的问题应该是“当你可以使用Tagged 时为什么要使用代理”(我现在将在这方面更新我的问题)。特别是因为“我要考虑类型化操作吗?” 的答案是:不,实际上a 不会在Schema 的左侧使用(它的所有构造函数)未参数化)。
  • @ChristianConkle 我查看了proxy a 位,此刻我明白这有什么好处,我同意这是一项“黑客工作”;)(只需使用 any i> * -> * 以某种方式“包装”内部类型,因为我们实际上并不关心包装器是什么。所以实际上该类型变量位本身是可以的,因为我们不在乎,但实例化包装器到 List 或任何其他具体类型,真的很尴尬。)
  • @nh2 不可能所有 Schema 的构造函数都是无类型的,因为 schema 本身就是。例如,如果您希望创建一个包含Schemas 的同质结构,那么该幻像变量将很快妨碍您。另一方面,如果您有Schema-combining 代数作用于类型化的表示,那么您可以使用幻像获得很多安全性。
  • 我更喜欢proxy 而不是Proxy,因为我经常可以访问可以与proxy a 统一的东西,例如Nothing[],可以用作直接论证而不是创建一个新的Proxy :: Proxy T,我认为它过于冗长。
猜你喜欢
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
  • 2019-04-02
  • 2010-10-30
  • 1970-01-01
  • 2018-05-29
  • 2021-07-16
  • 1970-01-01
相关资源
最近更新 更多