我不是varargs的最大粉丝,但我一直认为applicative(f <$> x <*> y
)和idiom([i| f x y |]
)样式的符号太多了。我通常更喜欢采用liftA2 f x y
方式,但我也认为A2有点难看。从this question开始,我了解到可以在Haskell中实现vararg函数。这样,是否可以使用相同的原理来实现提升功能,例如:
lift f a b == pure f <*> a <*> b
我已尝试在引用的代码上用+
替换<*>
:
class Lift r where
lift :: a -> r
instance Lift a where
lift = id
instance (Lift r) => Lift (a -> r) where
lift x y = lift (x <*> y)
但我无法让这些类型正确......
答案 0 :(得分:20)
请注意,您可以链接任意数量的<*>
,以获取表单
f (a0 -> .. -> an) -> (f a0 -> .. -> f an)
如果我们有a0 -> .. -> an
和f a0 -> .. -> f an
类型,我们可以从中计算f
。我们可以编码这种关系,以及最常见的类型,如下所示
class Lift a f b | a b -> f where
lift' :: f a -> b
正如您所料,“递归案例”实例只会应用<*>
一次,然后递归:
instance (a ~ a', f' ~ f, Lift as f rs, Applicative f)
=> Lift (a -> as) f (f' a' -> rs) where
lift' f a = lift' $ f <*> a
基本情况是没有更多功能的时候。由于您无法实际断言“a
不是函数类型”,因此这依赖于重叠实例:
instance (f a ~ b) => Lift a f b where
lift' = id
由于GHC实例选择规则,如果可能,将始终选择递归案例。
然后你想要的功能是lift' . pure
:
lift :: (Lift a f b, Applicative f) => a -> b
lift x = lift' (pure x)
这是Lift
的功能依赖变得非常重要的地方。由于f
仅在上下文中被提及,因此除非我们能够确定f
只知道a
和b
(它们出现在=>
)的右侧。
这需要几个扩展名:
{-# LANGUAGE
OverlappingInstances
, MultiParamTypeClasses
, UndecidableInstances
, FunctionalDependencies
, ScopedTypeVariables
, TypeFamilies
, FlexibleInstances
#-}
并且,与Haskell中的可变参数函数一样,通常选择实例的唯一方法是提供显式类型签名。
lift (\x y z -> x * y + z) readLn readLn readLn :: IO Int
我写这篇文章的方式,GHC很乐意接受lift
f
的参数,f
(而不是lift (+) [1..5] [3..5] :: (Enum a, Num a) => [a]
本身)。
main = lift (\x y z -> x * y + z) readLn readLn readLn >>= print
有时上下文足以推断出正确的类型。请注意,参数类型也是多态的。
OverlappingInstances
自GHC&gt; = 7.10起,OverlappingInstances
已被弃用,编译器将发出警告。它可能会在以后的某个版本中删除。这可以通过从{-# LANGUAGE .. #-}
pragma中删除instance {-# OVERLAPS #-} (f a ~ b) => Lift a f b where
并将第二个实例更改为
{{1}}
答案 1 :(得分:7)
我假设你更喜欢使用没有类型注释的lift
。在这种情况下,基本上有两种选择:
首先,如果我们使用OverlappingInstances
,多态函数需要注释:
{-# LANGUAGE
OverlappingInstances,
MultiParamTypeClasses,
UndecidableInstances,
FunctionalDependencies,
FlexibleInstances,
TypeFamilies
#-}
import Control.Applicative
class Applicative f => ApN f a b | a b -> f where
apN :: f a -> b
instance (Applicative f, b ~ f a) => ApN f a b where
apN = id
instance (Applicative f, ApN f a' b', b ~ (f a -> b')) => ApN f (a -> a') b where
apN f fa = apN (f <*> fa)
lift :: ApN f a b => a -> b
lift a = apN (pure a)
-- Now we can't write "lift (+) (Just 0) Nothing"
-- We must annotate as follows:
-- lift ((+) :: Int -> Int -> Int) (Just 0) Nothing
-- Monomorphic functions work fine though:
-- lift (||) (Just True) (Just True) --> results in "Just True"
第二次,如果我们改为使用IncoherentInstances
,lift
即使在多态函数上也可以在没有注释的情况下工作。但是,一些复杂的东西仍然无法检出,例如(lift . lift) (+) (Just (Just 0)) Nothing
。
{-# LANGUAGE
IncoherentInstances, MultiParamTypeClasses,
UndecidableInstances,ScopedTypeVariables,
AllowAmbiguousTypes, FlexibleInstances, TypeFamilies
#-}
import Control.Applicative
class Applicative f => ApN f a b where
apN :: f a -> b
instance (Applicative f, b ~ f a) => ApN f a b where
apN = id
instance (Applicative f, ApN f a' b', b ~ (f a -> b')) => ApN f (a -> a') b where
apN f fa = apN (f <*> fa)
lift :: forall f a b. ApN f a b => a -> b
lift a = (apN :: f a -> b) (pure a)
-- now "lift (+) (Just 0) (Just 10)" works out of the box
我提出了两个解决方案,而不仅仅是IncoherentInstances
解决方案,因为IncoherentInstances
是一个相当粗略的扩展,如果可能的话应该避免。这可能很好,但无论如何我认为提供替代解决方案是值得的。
在这两种情况下,我使用相同的技巧来帮助推理和减少注释:我尝试将信息从实例头移动到实例约束。而不是
instance (Applicative f) => ApN f a (f a) where
apN = id
我写
instance (Applicative f, b ~ f a) => ApN f a b where
apN = id
另外,在另一个实例中,我在实例头中使用了一个普通的b
参数,并将b ~ (f a ~ b')
添加到约束中。
这样做的原因是GHC首先检查是否存在匹配的实例头,并且只有在成功匹配后才尝试解析约束。我们希望在实例头上放置最少的负担,并让约束求解器对事物进行排序(因为它更灵活,可以延迟判断并可以使用程序其他部分的约束)。