Как понять подпись типа Haskell для оператора Control.Arrow '&&&'
Я пытаюсь обвести голову вокруг того, как работает Haskell Control.Arrow
&&&
, но боюсь, что я на пороге теряя мой путь.
В частности, я запутался (как новичок) в том, как понять его поведение от сигнатуры типа
(&&&) :: a b c -> a b c' -> a b (c, c')
в
import Control.Arrow
(negate &&& (+5)) <$> [1,2,3]
или даже просто
(negate &&& (+5)) 5
например, первый аргумент "отсутствует" b
и c
, а второй отсутствует только c'
, и результат выглядит мне как (c, c')
, а не a b (c, c')
.
Может ли кто-нибудь пропустить меня, как работает &&&
в контексте его типа?
Ответы
Ответ 1
Я всегда думаю о &&&
как о расколе и применении операции. У вас есть значение стрелки, и вы собираетесь применить две функции (извините, стрелки, но они работают с функциями и облегчают объяснения) и сохраняют оба результата, разделяя таким образом поток.
Мертвый простой пример:
λ> (succ &&& pred) 42
(43,41)
Прогуливаясь по типу, у нас есть
succ &&& pred :: Arrow a, Enum b => a b (b,b)
Более сложный пример, где не все b:
show &&& (== 42) :: Arrow a, Show b, Eq b, Num b => a b (String,Bool)
Итак, на простом английском языке: &&&
выполняет две функции и объединяет их в одну функцию, которая принимает его, применяет к ней обе функции и возвращает пару результатов.
Но это определяется стрелками, а не функциями. Тем не менее он работает точно так же: он принимает две стрелки и объединяет их в одну стрелку, которая принимает свой вход, применяет к ней обе стрелки и возвращает пару результатов.
arrowOne :: Arrow a => a b c
arrowTwo :: Arrow a => a b c'
arrowOne &&& arrowTwo :: Arrow a => a b (c,c')
Добавление: часть того, что, похоже, вас смущает, - это тип типа a
, который все еще появляется в сигнатурах типа. Основное правило здесь состоит в том, что оно работает так же, как и когда вы видите типы функций ->
: он показывает, пока он не применяется.
Я помню, как читал литературу по стрелке, которая написала стрелки как b ~> c
(обратите внимание на тильду не тире) вместо a b c
, чтобы сделать параллель с функциями более очевидными.
Ответ 2
Подпись гласит:
(&&&) :: Arrow a => a b c -> a b c' -> a b (c, c')
и (->)
- это экземпляр класса типа Arrow
. Итак, переписывая подпись, специализированную для (->)
, введите:
(&&&) :: ((->) b c) -> ((->) b c') -> ((->) b (c, c'))
in, infix form будет выглядеть так:
(b -> c) -> (b -> c') -> (b -> (c, c'))
что просто означает
(&&&) :: (b -> c) -- given a function from type `b` to type `c`
-> (b -> c') -- and another function from type `b` to type `c'`
-> (b -> (c, c')) -- returns a function which combines the result of
-- first and second function into a tuple
простая репликация будет:
(&:&) :: ((->) b c) -> ((->) b c') -> ((->) b (c, c'))
f &:& g = \x -> (f x, g x)
который будет работать одинаково:
\> (negate &:& (+5)) <$> [1, 2, 3]
[(-1,6),(-2,7),(-3,8)]
Ответ 3
У вас есть множество отличных ответов; Я просто хотел добавить, как понимать эту вещь, только глядя на синтаксис.
Правила для языка типа Haskell такие же, как и обычный язык Haskell: f a b = (f a) b
- это некоторая функция типа f
, применяемая к некоторому аргументу типа a
для создания другой применяемой функции типа f a
к некоторому аргументу типа b
. Тогда есть один оператор (и некоторые флаги компилятора позволяют вам включать другие) ->
, который обозначает тип функций, которые ожидают ввода первого типа, и возвращают второй тип в качестве вывода.
Таким образом, выражение (&&&) :: (Arrow a) => a b c -> a b c' -> a b (c, c')
означает "Тип функций от a b c
до функций от a b c'
до a b (c, c')
с добавленным ограничением, что a
является членом класса стилей Arrow.
Существует множество различных видов стрелок; это говорит: "Я беру две стрелки одного и того же типа, одну от b
до c
и другую от b
до c'
, и объединим их вместе со стрелкой того же сорта от b
до (c, c')
".
Интуитивно, мы бы сказали, что он объединяет две стрелки "параллельно", так что они действуют на одном и том же входе, производят разные выходы, а затем "присоединяются" к этим выходам с помощью нашей структуры данных с неоднородными парами (,)
.
В частном случае, когда стрелка (->)
, функция стрелка, это, очевидно, (f &&& g) = \b -> (f b, g b)
. Этот оператор (&&&)
является частью определения Arrow
typeclass, поэтому что-то может быть только Arrow
от одного типа к другому, если есть способ выполнить аналогичную "параллельную операцию" двух стрелок.
Ответ 4
Может быть проще пропустить некоторое объяснение того, как работают стрелки, и посмотреть тип (negate &&& (+5))
:
> :t (negate &&& (+5))
(negate &&& (+5)) :: Num a => a -> (a, a)
С negate :: a -> a
и (+5) :: a -> a
функция, созданная &&&
, принимает значение типа a
и возвращает пару значений, оба из которых имеют тип a
.
При рассмотрении функции как экземпляра Arrow
, b
и c
являются просто именами, указанными для аргументов и возвращаемых типов соответственно. То есть, если f :: b -> c
и g :: b -> c'
- две функции, которые принимают аргументы одного и того же типа, но возвращают значения, возможно, разных типов, то ( f &&& g ) :: b -> (c, c')
, что означает, что новая функция принимает одно значение общего типа ввода, а затем возвращает пара, состоящая из возвращаемых значений каждой из исходных функций или ( f &&& g ) x = (f x, g x)
.