基于两个不同函子的应用类型类

Applicative typeclass based on two different functors

是否有类似于 Applicative 类型的东西 class,但是应用程序的每一侧都有两个不同的仿函数?

(<*>) :: (Functor f, Functor g) => f (a -> b) -> g a -> f b

根据您的评论,我认为您可能试图构建:

import Data.Foldable
import Data.Traversable
foo :: (Traversable f, Foldable g) => f (a -> b) -> g a -> f b
foo f g = snd $ mapAccumR (\(a:as) fab -> (as, fab a)) (toList g) f

这允许,例如:

> import qualified Data.Vector as V
> foo [(+1),(+2),(+3)] (V.fromList [5,6,7])
[8,8,8]
> 

我不知道有什么通用的 fromList. 我会写一个具体的版本,或者最多概括一下输入类型。以下是 Vector 的示例,忽略 Data.Vector.zip 已经存在。

import qualified Data.Vector as V
import Data.Vector (Vector)
import Data.Foldable
import GHC.Exts (IsList(fromList))

zipV1 :: Vector (a -> b) -> Vector a -> Vector b
zipV1 fs as = V.fromList (zipWith ($) (V.toList fs) (V.toList as))

zipV2 :: (Foldable f, Foldable g, IsList (f b)) => f (a -> b) -> g a -> f b
zipV2 fs as = fromList (zipWith ($) (toList fs) (toList as))

您可以在第二个示例中使用 IsList 而不是 Foldable

"sequence type" 的一个一般概念是自由幺半群。由于您正在查看多态序列类型,我们可以在 Traversable.

的基础上构建
class Semigroup1 t where
  (<=>) :: t a -> t a -> t a

class Semigroup1 t => Monoid1 t where
  mempty1 :: t a

请参阅下面的注释。

class (Traversable t, Monoid1 t) => Sequence t where
  singleton :: a -> t a

那是一种序列类型吗?非常低效。但是我们可以添加一堆具有默认实现的方法来提高效率。以下是一些基本功能:

cons :: Sequence t => a -> t a -> t a
cons x xs = singleton x <=> xs

fromList
  :: (Foldable f, Sequence t)
  => f a -> t a
fromList = foldr cons mempty1

uncons :: Sequence t => t a -> Maybe (a, t a)
uncons xs = case toList xs of
  y:ys -> Just (y, fromList ys)
  [] -> Nothing

使用这些工具,您可以将任意两个序列压缩成第三个序列。

zipApp :: (Foldable t, Foldable u, Sequence v) = t (a -> b) -> u a -> v b
zipApp fs xs = fromList $ zipWith ($) (toList fs) (toList xs)

最近的 GHC 版本注意事项

对于最先进的 GHC,您可以使用 QuantifiedConstraintsRankNTypesConstraintKinds 并定义

type Semigroup1 t = forall a. Semigroup (t a)

type Monoid1 t = forall a. Monoid (t a)

这样做会让你写,例如,

fromList = foldMap singleton

(根据@dfeuer 在评论中的建议。)

有一种叫做 day convolution 的结构可以让你在执行应用操作时保留两个函子之间的区别,并延迟将一个函子转换为另一个函子的时间。

Day 类型只是一对函子值,以及一个组合它们各自结果的函数:

data Day f g a = forall b c. Day (f b) (g c) (b -> c -> a)

注意函子的实际 return 值是存在的; return 组合的值是函数的值。

Day 比其他组合应用函子的方法有优势。与Sum, the composition is still applicative. Unlike Compose, the composition is "unbiased" and doesn't impose a nesting order. Unlike Product不同的是,它让我们可以轻松地将applicative action与不同的 return类型结合起来,我们只需要提供一个合适的适配器函数即可。

例如,这里有两个 Day ZipList (Vec Nat2) Char 值:

{-# LANGUAGE DataKinds #-}
import           Data.Functor.Day -- from "kan-extensions"
import           Data.Type.Nat -- from "fin"
import           Data.Vec.Lazy -- from "vec"
import           Control.Applicative

day1 :: Day ZipList (Vec Nat2) Char
day1 = Day (pure ()) ('b' ::: 'a' ::: VNil) (flip const)

day2 :: Day ZipList (Vec Nat2) Char
day2 = Day (ZipList "foo") (pure ()) const

(Nat2 is from the fin package, it is used to parameterize a fixed-size Vec from vec.)

我们可以将它们压缩在一起就好了:

res :: Day ZipList (Vec Nat2) (Char,Char)
res = (,) <$> day1 <*> day2

然后将 Vec 转换为 ZipList 并折叠 Day:

res' :: ZipList (Char,Char)
res' = dap $ trans2 (ZipList . toList) res

ghci> res'
ZipList {getZipList = [('b','f'),('a','o')]}

使用 dap and trans2 函数。

可能的性能问题:当我们将其中一个函子提升到 Day 时,另一个函子被赋予一个虚拟的 pure () 值。但是,当 Day(<*>) 组合时,这是自重。可以通过将函子包装在 Lift 中来更智能地工作,以便为简单的 "pure" 案例获得更快的操作。