Haskell:如何创建最通用的函数,将函数应用于元组项 [英] Haskell: How to create most generic function possible that applies a function to tuple items

查看:18
本文介绍了Haskell:如何创建最通用的函数,将函数应用于元组项的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

This is a personal exercise to understand the limits of Haskell's type system a little better. I want to create the most generic function I can that applies some function to each entry in a 2 entry tuple eg:

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

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

I am trying to make this function work in each of the following cases:

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

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

So for length the items in the pair must be Foldable, for show they must be instances of Show etc.

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

Using RankNTypes I can go some of the way, for example:

{-# 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 将返回不同的类型).我当然可以定义一个上下文来放置项目,例如:

This allows a function that can work on a general context f to be applied to items in that context. (1) works with this, but the tuple items in (2) and (3) have no context and so they don't work (and anyway, 3 would return different types). I could of course define a context to place items in eg:

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 一个更具体的类型签名.

to get other examples working. I am just wondering whether it is possible to define such a generic function in Haskell without having to wrap the items in the tuples or give applyToTuple a more specific type signature.

推荐答案

你和上一个已经很接近了,但是你需要添加约束:

You were pretty close with the last one, but you need to add constraints:

{-# 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 是通过歧义检查所必需的.我认为这是因为它不知道从函数中使用约束的哪一部分.

The Proxy is necessary to pass the ambiguity check. I think this is because it wouldn't otherwise know which part of the constraint to use from the function.

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

In order to unify this with other cases, you need to allow empty constraints. It might be possible, but I'm not sure. You can't partially apply type families, which might make it a bit trickier.

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

This is a bit more flexible than I thought it would be though:

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

直到这一刻我才知道你可以部分应用类型相等,哈哈.

I had no idea you could partially apply type equality until this moment, haha.

不幸的是,这不起作用:

Unfortunately, this doesn't work:

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

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

For the particular case of lists though, we can use IsList from GHC.Exts to get this to work (IsList is usually used with the OverloadedLists extension, but we don't need that here):

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

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

Of course, the simplest (and even more general) solution is to use a function of type (a -> a') -> (b -> b') -> (a, b) -> (a', b') (like bimap from Data.Bifunctor or (***) from Control.Arrow) and just give it the same function twice:

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

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

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

Unifying all three examples from the question

Okay, after some more thought and coding, I figured out how to at least unify the three examples you gave into a single function. It's not the most intuitive thing maybe, but it seems to work. The trick is that, in addition to what we have above, we allow the function to give back two different result types (the elements of the resulting pair can be of different types) if we give the type system the following restriction:

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

Both result types must have a relation to the corresponding input type given by a two-parameter type class (we can look at a one parameter type class as a logical predicate on a type and we can look at a two parameter type class as capturing a binary relation between two types).

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

This is necessary for something like applyToTuple (+5) (10 :: Int, 2.3 :: Float), since it gives you back (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 代表我们输入和输出类型之间的关系.接下来,我们定义一个便利类(据我所知,它在任何地方都不存在):

Proxy p represents our relation between the input and output types. Next, we define a convenience class (which, as far as I know, doesn't exist anywhere already):

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:

This lets us use both when the result type stays the same by partially applying Constant to the type we know it will be (I also didn't know you could partially apply type classes until now. I'm learning a lot for this answer, haha). For example, if we know that it will be Int in both results:

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")

对于您的第二个测试用例也是如此:

Likewise for your second test case:

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)

第三个是更有趣的地方:

The third one is where it gets a bit more interesting:

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)

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

Our relation between input and output type here is actually only slightly more complex than the other two examples: instead of ignoring the first type in the relation, we say that the input and output types must be the same (which works since (+5) :: Num a => a -> a). In other words, in this particular case, our relation is the equality relation.

这篇关于Haskell:如何创建最通用的函数,将函数应用于元组项的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆