Haskell:如何创建将函数应用于元组项的最通用函数

jsd*_*sdw 16 polymorphism haskell

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

applyToTuple fn (a,b) = (fn a, fn b)
Run Code Online (Sandbox Code Playgroud)

我试图使这个功能在以下每种情况下都有效:

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

因此对于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)
Run Code Online (Sandbox Code Playgroud)

这允许可以在一般上下文中工作的函数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')
Run Code Online (Sandbox Code Playgroud)

让其他例子有效.我只是想知道是否可以在Haskell中定义这样的泛型函数,而不必将元素包装在元组中,或者为applyToTuple提供更具体的类型签名.

Dav*_*vid 15

你与最后一个非常接近,但你需要添加约束:

{-# 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)
Run Code Online (Sandbox Code Playgroud)

Proxy通过模糊检查是必要的.我认为这是因为它不会知道从函数中使用哪个约束部分.

为了将其与其他情况统一起来,您需要允许空约束.这可能是可能的,但我不确定.您不能部分应用类型系列,这可能会使它有点棘手.

这比我想象的要灵活得多:

demo2 :: (Char, Char)
demo2 = both (Proxy :: Proxy ((~) Char)) id ('a', 'b')
Run Code Online (Sandbox Code Playgroud)

哈哈,我不知道你可以部分应用类型平等.

不幸的是,这不起作用:

demo3 :: (Int, Int)
demo3 = both (Proxy :: Proxy ((~) [a])) length ([1,2,3::Int], "hello")
Run Code Online (Sandbox Code Playgroud)

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

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

当然,最简单(甚至更一般)的解决方案是使用类型的函数(a -> a') -> (b -> b') -> (a, b) -> (a', b')(如bimapfromData.Bifunctor(***)fromControl.Arrow)并只给它两次相同的函数:

?> bimap length length ([1,2,3], "hello")
(3,5)
Run Code Online (Sandbox Code Playgroud)

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

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

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

这是必要的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)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

这让我们可以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")
Run Code Online (Sandbox Code Playgroud)

同样,对于您的第二个测试用例

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)
Run Code Online (Sandbox Code Playgroud)

第三个是更有趣的地方:

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)
Run Code Online (Sandbox Code Playgroud)

这里我们的输入和输出类型之间的关系实际上只比其他两个例子稍微复杂一点:我们说输入和输出类型必须相同(从而起作用(+5) :: Num a => a -> a),而不是忽略关系中的第一个类型.换句话说,在这种特殊情况下,我们的关系是平等关系.