Я не знаю учебник, но я думаю, что стрелки легче всего понять, если вы посмотрите на некоторые конкретные примеры.Самая большая проблема, с которой я столкнулся, изучая, как использовать стрелки, заключалась в том, что ни один из учебных пособий или примеров на самом деле не показал, как использовать стрелки, а только как их составить.Итак, учитывая это, вот мой мини-учебник.Я рассмотрю две разные стрелки: функции и пользовательский тип стрелки MyArr
.
-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))
1) Стрелка - это вычисление от ввода указанного типа до вывода указанного типа.Класс типов стрелки принимает три аргумента типа: тип стрелки, тип ввода и тип вывода.Рассматривая заголовок экземпляра для экземпляров стрелок, мы находим:
instance Arrow (->) b c where
instance Arrow MyArr b c where
Стрелка (либо (->)
, либо MyArr
) является абстракцией вычисления.
Для функции b -> c
, b
- это вход, а c
- это выход.
Для MyArr b c
, b
- это вход, а c
- это выход.
2) Для фактического запускавычисление стрелки, вы используете функцию, специфичную для вашего типа стрелки.Для функций вы просто применяете функцию к аргументу.Для других стрелок должна быть отдельная функция (например, runIdentity
, runState
и т. Д. Для монад).
-- run a function arrow
runF :: (b -> c) -> b -> c
runF = id
-- run a MyArr arrow, discarding the remaining computation
runMyArr :: MyArr b c -> b -> c
runMyArr (MyArr step) = fst . step
3) Стрелки часто используются для обработки списка входных данных.Для функций это может быть сделано параллельно, но для некоторых стрелок вывод на любом данном шаге зависит от предыдущих входов (например, сохранение текущего количества входов).
-- run a function arrow over multiple inputs
runFList :: (b -> c) -> [b] -> [c]
runFList f = map f
-- run a MyArr over multiple inputs.
-- Each step of the computation gives the next step to use
runMyArrList :: MyArr b c -> [b] -> [c]
runMyArrList _ [] = []
runMyArrList (MyArr step) (b:bs) = let (this, step') = step b
in this : runMyArrList step' bs
Это одна из причин, по которой стрелки полезны.Они предоставляют вычислительную модель, которая может неявно использовать состояние, даже не выставляя это состояние программисту.Программист может использовать стрелочные вычисления и комбинировать их для создания сложных систем.
Вот MyArr, который ведет подсчет количества полученных входов:
-- count the number of inputs received:
count :: MyArr b Int
count = count' 0
where
count' n = MyArr (\_ -> (n+1, count' (n+1)))
Теперь функция runMyArrList count
примет длину списка n в качестве входных данных и вернет список значений Ints от 1 до n.
Обратите внимание, что мы до сих пор не использовали никаких функций "стрелок", то есть методов класса Arrow или функций, написанных в терминахиз них.
4) Большая часть кода выше специфична для каждого экземпляра Arrow [1].Все в Control.Arrow
(и Control.Category
) - это составление стрелок для создания новых стрелок.Если мы представим, что Category является частью Arrow, а не отдельным классом:
-- combine two arrows in sequence
>>> :: Arrow a => a b c -> a c d -> a b d
-- the function arrow instance
-- >>> :: (b -> c) -> (c -> d) -> (b -> d)
-- this is just flip (.)
-- MyArr instance
-- >>> :: MyArr b c -> MyArr c d -> MyArr b d
Функция >>>
берет две стрелки и использует вывод первой в качестве ввода для второй.
Вот еще один оператор, обычно называемый "fanout":
-- &&& applies two arrows to a single input in parallel
&&& :: Arrow a => a b c -> a b c' -> a b (c,c')
-- function instance type
-- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c'))
-- MyArr instance type
-- &&& :: MyArr b c -> MyArr b c' -> MyArr b (c,c')
-- first and second omitted for brevity, see the accepted answer from KennyTM's link
-- for further details.
Поскольку Control.Arrow
предоставляет средства для объединения вычислений, вот один пример:
-- function that, given an input n, returns "n+1" and "n*2"
calc1 :: Int -> (Int,Int)
calc1 = (+1) &&& (*2)
Я часто находил функциинапример, calc1
, полезный в сложных сгибах, или функции, которые работают, например, с указателями.
Класс типа Monad
предоставляет нам средство для объединения монадических вычислений в одно новое монадическое вычисление с использованием >>=
функция.Точно так же класс Arrow
предоставляет нам средства для объединения вычислений со стрелками в одно новое вычисление со стрелками с использованием нескольких примитивных функций (first
, arr
и ***
, с >>>
и id
из Control.Category).Также похож на монады, вопрос "Что делает стрела?"не может быть общего ответа.Это зависит от стрелки.
К сожалению, я не знаю многих примеров экземпляров стрелок в дикой природе.Функции и FRP кажутся наиболее распространенными приложениями.HXT - единственное другое важное использование, которое приходит на ум.
[1] За исключением count
.Можно написать функцию подсчета, которая делает то же самое для любого экземпляра ArrowLoop
.