Haskell:为什么使用代理?

标签 haskell

在 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 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 的类型签名要复杂得多,那么使用代理将会是一个很大的改进。

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

代理标记

在我的所有示例中,类型参数 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/

相关文章:

scala - 有哪些类型级编程的例子?

haskell - 在 Haskell 中,范围 ['a' ..] 在哪里停止?

python - 好的声音库?

在 where 子句中的 Haskell 类型映射(-> 运算符)?

haskell - 转换单子(monad)

haskell - 切换参数顺序的函数的类型是什么?

Peter Norvig 的拼写校正器的 Haskell 版本慢得令人难以置信

haskell - 为什么 Haskell 将我的 Num 类型解释为 Enum?

不透明数据类型的 Haskell Data 实例

haskell - 我是否正确地思考和使用 Haskell 中的单例类型?