【问题标题】:Haskell: How to create most generic function possible that applies a function to tuple itemsHaskell:如何创建将函数应用于元组项的最通用函数
【发布时间】:2015-07-04 12:58:10
【问题描述】:

这是一个个人练习,可以更好地理解 Haskell 类型系统的局限性。我想创建最通用的函数,将一些函数应用于 2 条目元组中的每个条目,例如:

applyToTuple fn (a,b) = (fn a, fn b)

我正在尝试使此功能在以下每种情况下都能正常工作:

(1) applyToTuple length ([1,2,3] "hello")
(2) applyToTuple show ((2 :: Double), 'c')
(3) applyToTuple (+5) (10 :: Int, 2.3 :: Float)

所以对于length,对中的项目必须是Foldable,为了显示它们必须是Show等的实例。

使用RankNTypes我可以走一些路,例如:

{-# LANGUAGE RankNTypes #-}
applyToTupleFixed :: (forall t1. f t1 -> c) -> (f a, f b) -> (c, c)
applyToTupleFixed fn (a,b) = (fn a, fn b)

这允许可以在一般上下文f 上工作的函数应用于该上下文中的项目。 (1) 适用于此,但 (2)(3) 中的元组项没有上下文,因此它们不起作用(无论如何,3 会返回不同的类型)。我当然可以定义一个上下文来放置项目,例如:

data Sh a = Show a => Sh a
instance Show (Sh a) where show (Sh a) = show a

applyToTuple show (Sh (2 :: Double), Sh 'c')

让其他示例正常工作。我只是想知道是否可以在 Haskell 中定义这样一个通用函数,而不必将项目包装在元组中或给 applyToTuple 一个更具体的类型签名。

【问题讨论】:

  • 查找BifunctorTrifunctor等。
  • 马上开始,我的印象是这些函数每个元组项都有一个函数(查看了它的元组实例),这避免了上述问题。如果我错了,请纠正我!

标签: haskell polymorphism


【解决方案1】:

您与上一个非常接近,但您需要添加约束:

{-# LANGUAGE RankNTypes      #-}
{-# LANGUAGE ConstraintKinds #-}
import Data.Proxy

both :: (c a, c b)
     => Proxy c
        -> (forall x. c x => x -> r)
        -> (a, b)
        -> (r, r)
both Proxy f (x, y) = (f x, f y)

demo :: (String, String)
demo = both (Proxy :: Proxy Show) show ('a', True)

Proxy 是通过歧义检查所必需的。我认为这是因为它不知道要从函数中使用约束的哪一部分。

为了与其他情况统一,您需要允许空约束。这可能是可能的,但我不确定。您不能部分应用类型族,这可能会有点棘手。

这比我想象的要灵活一些:

demo2 :: (Char, Char)
demo2 = both (Proxy :: Proxy ((~) Char)) id ('a', 'b')

直到现在我才知道你可以部分应用类型相等,哈哈。

很遗憾,这不起作用:

demo3 :: (Int, Int)
demo3 = both (Proxy :: Proxy ((~) [a])) length ([1,2,3::Int], "hello")

对于列表的特殊情况,我们可以使用GHC.Exts 中的IsList 来使其工作(IsList 通常与OverloadedLists 扩展一起使用,但我们在这里不需要):

demo3 :: (Int, Int)
demo3 = both (Proxy :: Proxy IsList) (length . toList) ([1,2,3], "hello")

当然,最简单(甚至更通用)的解决方案是使用(a -> a') -> (b -> b') -> (a, b) -> (a', b') 类型的函数(如bimap from Data.Bifunctor(***) from Control.Arrow),然后给它两次相同的函数:

λ> bimap length length ([1,2,3], "hello")
(3,5)

统一问题中的所有三个示例

好的,经过更多的思考和编码,我想出了如何至少将您给出的三个示例统一到一个函数中。这可能不是最直观的事情,但它似乎有效。诀窍是,除了我们上面的内容之外,如果我们给类型系统提供以下限制,我们允许函数返回两种不同的结果类型(结果对的元素可以是不同的类型):

两种结果类型都必须与双参数类型类给出的相应输入类型有关系(我们可以将单参数类型类视为类型的逻辑谓词,我们可以查看双参数类型类作为捕获两种类型之间的二元关系)。

这对于applyToTuple (+5) (10 :: Int, 2.3 :: Float) 之类的东西是必要的,因为它会返回(Int, Float)

这样,我们得到:

{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Data.Proxy

import GHC.Exts

both :: (c a, c b
        ,p a r1  -- p is a relation between a and r1
        ,p b r2  -- and also a relation between b and r2
        )
     => Proxy c
        -> Proxy p
        -> (forall r x. (c x, p x r) => x -> r) -- An input type x and a corresponding
                                                -- result type r are valid iff the p from
                                                -- before is a relation between x and r,
                                                -- where x is an instance of c
        -> (a, b)
        -> (r1, r2)
both Proxy Proxy f (x, y) = (f x, f y)

Proxy p 表示输入和输出类型之间的关系。接下来,我们定义一个便利类(据我所知,它在任何地方都不存在):

class r ~ a => Constant a b r
instance Constant a b a      -- We restrict the first and the third type argument to
                             -- be the same

这让我们可以在结果类型保持不变时使用both,方法是将Constant 部分应用于我们知道的类型(我也不知道您可以部分应用类型类,直到现在。我为这个答案学到了很多东西,哈哈)。例如,如果我们知道在两个结果中都是Int

example1 :: (Int, Int)
example1 =
  both (Proxy :: Proxy IsList)         -- The argument must be an IsList instance
       (Proxy :: Proxy (Constant Int)) -- The result type must be Int
       (length . toList)
       ([1,2,3], "hello")

第二个测试用例也是如此:

example2 :: (String, String)
example2 =
  both (Proxy :: Proxy Show)              -- The argument must be a Show instance
       (Proxy :: Proxy (Constant String)) -- The result type must be String
       show
       ('a', True)

第三个是更有趣的地方:

example3 :: (Int, Float)
example3 =
  both (Proxy :: Proxy Num)  -- Constrain the the argument to be a Num instance
       (Proxy :: Proxy (~))  -- <- Tell the type system that the result type of
                             --    (+5) is the same as the argument type.
       (+5)
       (10 :: Int, 2.3 :: Float)

我们这里的输入和输出类型之间的关系实际上只比其他两个例子稍微复杂一点:我们不是忽略关系中的第一个类型,而是说输入和输出类型必须相同(这在 @ 987654349@)。换句话说,在这种特殊情况下,我们的关系是等式关系。

【讨论】:

  • 谢谢你,一个有趣的答案让我有更多的了解:)
  • @jsdw 好的,它有点复杂,但我想出了如何编写一个适用于问题中所有三个示例的函数,我更新了我的答案。
  • 感谢您抽出宝贵的时间。我做了一些阅读,ConstraintKinds 看起来很棒。本质上,约束可以用作类型,而 Proxy 是一种非常简单的数据类型(一个衬里),用于将这些约束类型放入一个函数中,以便我们可以在其中用作约束。 Haskell,你继续让我心烦意乱。
【解决方案2】:

你想要的是一个有类型的函数

applyToTuple :: (a -> b) -> (c, d) -> (b, b)

编译器将检查acd 是否在同一个类型类中。不幸的是,据我所知这是不可能的(尽管在某处可能会有扩展)。当您将某个类型类的函数传递给另一个函数时,它的类型将成为它应用到的第一个对象的类型(来自 GHC 的观察):

applyToTuple f (x, y) = (f x, f y)

有一个派生类型applyToTuple :: (t -&gt; t1) -&gt; (t, t) -&gt; (t1, t1)。 使用show 对其进行测试显示了以下结果:

λ> applyToTuple show (8, 9)
("8","9")
λ> applyToTuple show (8, [8,9])

<interactive>:5:14:
    No instance for (Show t0) arising from a use of `show'
    The type variable `t0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Show Double -- Defined in `GHC.Float'
      instance Show Float -- Defined in `GHC.Float'
      instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus 28 others
    In the first argument of `applyToTuple', namely `show'
    In the expression: applyToTuple show (8, [8, 9])
    In an equation for `it': it = applyToTuple show (8, [8, 9])

<interactive>:5:20:
    No instance for (Num [t0]) arising from the literal `8'
    Possible fix: add an instance declaration for (Num [t0])
    In the expression: 8
    In the second argument of `applyToTuple', namely `(8, [8, 9])'
    In the expression: applyToTuple show (8, [8, 9])

<interactive>:5:24:
    No instance for (Num t0) arising from the literal `8'
    The type variable `t0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 8
    In the expression: [8, 9]
    In the second argument of `applyToTuple', namely `(8, [8, 9])'

但是,您可以执行applyToTuple' f1 f2 (x, y) = (f1 x, f2 y) 之类的操作。我认为您可以使用Template Haskell 将其转换为您想要的。

【讨论】:

    猜你喜欢
    • 2014-11-23
    • 1970-01-01
    • 2016-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多