import Control.Applicative
import Control.Arrow
filter ((&&) <$> (>2) <*> (<7)) [1..10]
filter ((>2) &&& (<7) >>> uncurry (&&)) [1..10]
两者都得到相同的结果!但是,我很难理解。有人可以在这里详细解释一下吗?
答案 0 :(得分:23)
让我们从第二个开始,这更简单。我们这里有两个神秘的操作符,具有以下类型:
(&&&) :: Arrow a => a b c -> a b c' -> a b (c, c')
(>>>) :: Category cat => cat a b -> cat b c -> cat a c
Arrow
和Category
类型类主要是关于行为类似函数的事物,当然包括函数本身,这里的两个实例都只是普通的(->)
。所以,重写要使用的类型:
(&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c'))
(>>>) :: (a -> b) -> (b -> c) -> (a -> c)
第二种类型与(.)
非常相似,是熟悉的函数组合运算符;事实上,它们是相同的,只是交换了参数。第一个是比较陌生的,但类型再次告诉你所有你需要知道的东西 - 它需要两个函数,都采用一个普通类型的参数,并产生一个函数,将两者的结果组合成一个元组。 / p>
因此,表达式(>2) &&& (<7)
只取一个数字,并根据比较产生一对Bool
值。然后将结果输入uncurry (&&)
,它只需要一对Bool
和AND它们在一起。结果谓词用于以通常的方式过滤列表。
第一个更神秘。我们还有两个神秘的运算符,它们具有以下类型:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
观察到(<$>)
在这种情况下的第二个参数是(>2)
,其类型为(Ord a, Num a) => a -> Bool
,而(<$>)
的参数类型为{{1} }}。这些是如何兼容的?
答案是,正如我们可以在早期类型签名中替换f a
(->)
和a
一样,我们可以将cat
视为a -> Bool
},并将(->) a Bool
替换为((->) a)
。因此,重写类型,改为使用f
来避免与其他类型变量((->) t)
发生冲突:
a
现在,将内容恢复为正常的中缀形式:
(<$>) :: (a -> b) -> ((->) t) a -> ((->) t) b
(<*>) :: ((->) t) (a -> b) -> ((->) t) a -> ((->) t) b
第一个结果是函数组合,你可以从类型中观察到。第二个更复杂,但是再次类型告诉你你需要什么 - 它需要两个函数,一个是普通类型的参数,一个产生一个函数,另一个产生一个参数传递给函数。换句话说,像(<$>) :: (a -> b) -> (t -> a) -> (t -> b)
(<*>) :: (t -> (a -> b)) -> (t -> a) -> (t -> b)
之类的东西。 (这个函数恰好也被称为组合逻辑中的S combinator ,这是一个由逻辑学家Haskell Curry广泛探讨的主题,其名称无疑似乎很奇怪!)
\f g x -> f x (g x)
和(<$>)
的组合“扩展”(<*>)
单独执行的操作,在这种情况下意味着使用具有两个参数的函数,两个函数具有公共参数类型,将单个值应用于后两个,然后将第一个函数应用于两个结果。因此(<$>)
简化为((&&) <$> (>2) <*> (<7)) x
,或使用普通中缀样式(&&) ((>2) x) ((<7) x)
。和以前一样,复合表达式用于以通常的方式过滤列表。
另外,请注意,虽然这两个函数在某种程度上都是混淆的,但是一旦习惯了所使用的运算符,它们实际上就具有很强的可读性。第一个抽象的复合表达式对单个参数执行多个操作,而第二个抽象是标准“管道”样式的通用形式,将事物与函数组合串联起来。
我个人认为第一个完全可读。但我不希望大多数人同意!