Ответ 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
. Как и прежде, составное выражение используется для фильтрации списка обычным способом.
Также обратите внимание, что хотя обе функции в какой-то степени запутываются, как только вы привыкаете к используемым операторам, они на самом деле вполне читаемы. Первые абстракции над составным выражением, делающим несколько вещей одним аргументом, а второй - обобщенной формой стандартного "конвейерного" стиля наложения вещей вместе с композицией функций.
Лично я на самом деле нахожу первый, который отлично читается. Но я не ожидаю, что большинство людей согласятся!