是否可以编码通用"电梯"在Haskell中的功能?

时间:2015-01-17 19:01:24

标签: haskell typeclass applicative

我不是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)

但我无法让这些类型正确......

2 个答案:

答案 0 :(得分:20)

请注意,您可以链接任意数量的<*>,以获取表单

的功能
f (a0 -> .. -> an) -> (f a0 -> .. -> f an)

如果我们有a0 -> .. -> anf 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只知道ab(它们出现在=>)的右侧。

这需要几个扩展名:

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

第二次,如果我们改为使用IncoherentInstanceslift即使在多态函数上也可以在没有注释的情况下工作。但是,一些复杂的东西仍然无法检出,例如(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首先检查是否存在匹配的实例头,并且只有在成功匹配后才尝试解析约束。我们希望在实例头上放置最少的负担,并让约束求解器对事物进行排序(因为它更灵活,可以延迟判断并可以使用程序其他部分的约束)。