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

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

问题描述

这是一个个人的练习,它可以更好地理解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)
长度
,这对货品必须为 >

>可折叠
,为了表明它们必须是 Show 等的实例。



使用<$例如:

  { - #c $ c> 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 
实例Show(Sh a)Show(Sh a)= show a

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

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

解决方案

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

  { - #LANGUAGE RankNTypes# - } 
{ - #LANGUAGE Con​​straintKinds# - }
import Data.Proxy

both ::(ca,cb)
=>代理c
- > (forall x.c x => x - > r)
- > (a,b)
- > (r,r)
代理f(x,y)=(fx,fy)

demo ::(String,String)
demo = both(Proxy ::代理显示)show('a',True)

code>是通过模糊检查所必需的。我认为这是因为它不会知道从函数中使用哪一部分约束。



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



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


$ b $

  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)

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

$ $ p $ 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长度长度([1,2,3],hello)
(3,5)



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



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


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

对于像 applyToTuple(+5)(10 :: Int,2.3 :: Float)之类的东西是必须的,因为它可以让你回到(Int,Float )



$ b $ { - #LANGUAGE RankNTypes# - }
{ - #LANGUAGE Con​​straintKinds# - }
{ - #LANGUAGE FlexibleInstances# - }
{ - #LANGUAGE MultiParamTypeClasses# - }
导入Data.Proxy

import GHC.Exts

both ::(ca,cb
,pa r1 - p是a与r1
,pb r2之间的关系 - 也是a b和r2之间的关系

=>代理c
- >代理p
- > (如果rx。(cx,pxr)=> x - > r) - 输入类型x和相应的
- 结果类型r是有效的,如果p从
- before是x和r之间的关系,
- 其中x是c
- >的一个实例。 (a,b)
- > (r1,r2)
Proxy Proxy f(x,y)=(fx,fy)

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

pre code class r 〜a =>常量abr
实例常量aba - 我们将第一个和第三个参数限制为
- 是相同的

当结果类型通过部分应用常量 >我们知道它的类型(我也不知道你可以部分应用类型类,直到现在我正在学习很多这个答案,哈哈)。例如,如果我们知道它在两个结果中都是 Int

  example1 ::(Int,Int)
example1 =
both(Proxy :: Proxy IsList) - 参数必须是IsList实例
(Proxy :: Proxy(Constant Int) ) - 结果类型必须是Int
(length。toList)
([1,2,3],hello)

同样对于你的第二个测试用例:

  example2 ::(String ,String)
example2 =
both(Proxy :: Proxy Show) - 参数必须是Show实例
(Proxy :: Proxy(常量字符串)) - 结果类型必须be字符串
显示
('a',True)

第三个它是一个更有趣的地方:

$ p $ example3 ::(Int,Float)
example3 =
both(Proxy :: Proxy Num) - 将参数限制为Num实例
(Proxy :: Proxy(〜)) - < ; - 告诉类型系统
- (+5)的结果类型与参数类型相同。
(+5)
(10 :: Int,2.3 :: Float)

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


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)

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

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)

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

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)

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

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

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

This is necessary for something like applyToTuple (+5) (10 :: Int, 2.3 :: Float), since it gives you back (Int, Float).

With this, we get:

{-# 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 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

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)

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天全站免登陆