Подтвердить что ты не робот

Как понять подпись типа 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').

Может ли кто-нибудь пропустить меня, как работает &&& в контексте его типа?

4b9b3361

Ответ 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).