Не могли бы вы объяснить код на форуме?

import Control.Applicative
import Control.Arrow

filter ((&&) <$> (>2) <*> (<7)) [1..10]  
filter ((>2) &&& (<7) >>> uncurry (&&)) [1..10]

Оба получат тот же результат! Однако мне очень сложно понять. Может ли кто-нибудь здесь объяснить это подробно?

Ответ 1

Начнем со второго, что проще. Здесь у нас есть два таинственных оператора со следующими типами:

(&&&) :: 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)

Второй имеет очень похожий тип на (.), знакомый оператор композиции функций; на самом деле, они одинаковы, просто с аргументами. Первое более незнакомо, но типы снова сообщают вам все, что вам нужно знать, - он принимает две функции: как принимать аргумент общего типа, так и создает одну функцию, которая дает результаты из обоих объединений в кортеж.

Таким образом, выражение (>2) &&& (<7) принимает одно число и создает пару значений Bool на основе сравнений. Результат этого затем подается в uncurry (&&), который просто берет пару Bool и ANDs вместе. Получившийся предикат используется для фильтрации списка обычным способом.


Первый из них более загадочный. У нас есть два таинственных оператора, опять же, со следующими типами:

(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Обратите внимание, что второй аргумент (<$>) в этом случае равен (>2), который имеет тип (Ord a, Num a) => a -> Bool, а тип аргумента (<$>) имеет тип 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)

Первое оказывается составной функцией, как вы можете наблюдать из типов. Второе сложнее, но еще раз типы сообщают вам, что вам нужно, - он принимает две функции с аргументом общего типа, один из которых выполняет функцию, а другой - аргумент для перехода к функции. Другими словами, что-то вроде \f g x -> f x (g x). (Эта функция также известна как комбинация S в комбинационной логике, тема, подробно изученная логиком Haskell Curry, имя которого, без сомнения, кажется странным знаком!)

Комбинация (<$>) и (<*>) рода "расширяет" то, что делает только (<$>), что в данном случае означает принятие функции с двумя аргументами, две функции с общим типом аргумента, применяя одно значение к вторых, затем применяя первую функцию к двум результатам. Таким образом, ((&&) <$> (>2) <*> (<7)) x упрощается до (&&) ((>2) x) ((<7) x) или использует обычный стиль infix, x > 2 && x < 7. Как и прежде, составное выражение используется для фильтрации списка обычным способом.


Также обратите внимание, что хотя обе функции в какой-то степени запутываются, как только вы привыкаете к используемым операторам, они на самом деле вполне читаемы. Первые абстракции над составным выражением, делающим несколько вещей одним аргументом, а второй - обобщенной формой стандартного "конвейерного" стиля наложения вещей вместе с композицией функций.

Лично я на самом деле нахожу первый, который отлично читается. Но я не ожидаю, что большинство людей согласятся!