这是一个个人练习,可以更好地理解 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 一个更具体的类型签名。
最佳答案
您与最后一个非常接近,但您需要添加约束:
{-# 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")
不过,对于列表的特殊情况,我们可以使用
IsList
来自 GHC.Exts
让它工作( 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)
统一问题中的所有三个示例
好的,经过更多的思考和编码,我想出了如何至少将您给出的三个示例统一到一个函数中。这可能不是最直观的事情,但它似乎有效。诀窍是,除了我们上面的内容之外,如果我们给类型系统以下限制,我们允许函数返回两种不同的结果类型(结果对的元素可以是不同的类型):
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)
.有了这个,我们得到:
{-# 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)
我们这里的输入和输出类型之间的关系实际上只比其他两个例子稍微复杂一点:我们不是忽略关系中的第一个类型,而是说输入和输出类型必须相同(从
(+5) :: Num a => a -> a
开始有效) .换句话说,在这种特殊情况下,我们的关系是等式关系。
关于Haskell:如何创建将函数应用于元组项的最通用函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31220903/