Что такое стрелы, и как я могу их использовать?
Я пытался узнать смысл стрелки, но я их не понял.
Я использовал учебник Wikibooks. Я думаю, что проблема Wikibook в основном заключается в том, что она, похоже, написана для кого-то, кто уже понимает эту тему.
может кто-нибудь объяснить, что такое стрелки и как я могу их использовать?
5 ответов:
я не знаю учебник, но я думаю, что это проще всего понять стрелки, Если вы посмотрите на некоторые конкретные примеры. Самая большая проблема, которую я узнал, как использовать стрелки, заключалась в том, что ни один из учебников или примеров на самом деле не показывает, как использовать стрелки, как раз как составить их. Итак, имея это в виду, вот мой мини-учебник. Я рассмотрю две разные стрелки: функции и пользовательский тип стрелки
MyArr.-- type representing a computation data MyArr b c = MyArr (b -> (c,MyArr b c))1) Стрелка-это расчет из входных данных указанного типа для вывода указанного типа. Класс arrow typeclass принимает три аргумента типа: Тип стрелки, Тип ввода и тип вывода. Глядя на голову экземпляра для экземпляров arrow, мы находим:
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 . step3) стрелки часто используются для обработки входных сигналов. Для функций это может быть сделано параллельно, но для некоторых стрелок выход на любом заданном шаге зависит от предыдущих входов (например, сохранение текущего общего количества входов).
-- 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 в качестве входных данных и возвращает список целых чисел от 1 до n.обратите внимание, что мы до сих пор не использовали никаких функций "стрелка", то есть либо методы класса Arrow, либо функции, написанные в их терминах.
4) Большая часть кода выше специфична для каждого экземпляра стрелки[1]. Все в
Control.Arrow(иControl.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 dThe
>>>функция принимает две стрелки и использует выход первой в качестве входа для второй.вот еще один оператор, обычно называемый "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полезно в сложных складках, или функции, которые работают на указатели, например.The
Monadтип класс предоставляет нам средства для объединения монадические вычисления в одно новое монадическое вычисление с использованием . Точно так же,Arrowкласс предоставляет нам средства для объединения стрелочных вычислений в одно новое стрелочное вычисление с использованием нескольких примитивных функций (first,arrи***С>>>иidиз-под контроля.Категория.) Также подобно монадам, вопрос "что делает стрела?- вообще-то ответить на этот вопрос невозможно. Это зависит от стрелки.К Сожалению, Я не знаю многих примеров экземпляров стрелок в дикой природе. Функции и FRP, по-видимому, являются наиболее распространенными приложениями. HXT-это единственное другое значительное использование, которое приходит на ум.
[1] за исключением
count. Можно написать функцию count, которая делает то же самое для любого экземпляраArrowLoop.
от взгляда на вашу историю на переполнение стека, я собираюсь предположить, что вы комфортно с некоторыми другими стандартными классами типа, особенно
FunctorиMonoid, и начнем с краткой аналогии из них.единственная операция на
Functorиfmap, который служит обобщенной версиейmapв списках. Это в значительной степени вся цель класса type; он определяет "вещи, которые вы можете сопоставить". Так что, в некотором смыслеFunctorпредставляет собой обобщение этого конкретного аспекта списков.операции
Monoidявляются обобщенными версиями пустого списка и(++), и он определяет "вещи, которые могут быть объединены ассоциативно, с определенной вещью, которая является значением идентичности". Списки-это в значительной степени самая простая вещь, которая соответствует этому описанию, иMonoidпредставляет собой обобщение этого аспекта списки.таким же образом, как и выше два, операции на
Categoryтип класса являются обобщенными версиямиidи(.), и он определяет "вещи, соединяющие два типа в определенном направлении, которые могут быть подключены голова к хвосту". Итак, это представляет собой обобщение этого аспекта функции. В частности, не включены в обобщение карри или функции приложения.The
Arrowтип класса строится отCategory, но основная концепция та же:Arrows-это вещи, которые составляют как функции и имеют "identity arrow" определяется для любого типа. Дополнительные операции, определенные наArrowсам класс просто определяет способ поднять произвольную функцию доArrowи способ объединить две стрелки "параллельно" в виде одной стрелки между кортежами.Итак, первое, что нужно иметь в виду вот это выражения здании
Arrows-это, по существу, сложная функциональная композиция. Комбинаторы, как(***)и(>>>)предназначены для написания стиля " pointfree, в то время какprocнотация дает возможность назначать временные имена входам и выходам при подключении вещей.здесь полезно отметить, что, хотя
Arrows иногда описываются как "следующий шаг" отMonads, там действительно не очень значимые отношения. Для любогоMonadвы можете работать со стрелками Клейсли, которые просто функции с типом, какa -> m b. Элемент(<=<)операторControl.Monadявляется композиция стрелки для них. На с другой стороны,Arrows не получить вамMonadесли вы также включать тегиArrowApplyкласса. Так что прямой связи как таковой нет.ключевое отличие здесь в том, что тогда как
Monads можно использовать для последовательных вычислений и делать вещи шаг за шагом,Arrows в некотором смысле "вневременные", как и обычные функции. Они могут включать в себя дополнительное оборудование и функциональность, которая соединяется с помощью(.), но это больше похоже на строительство трубопровода, а не накопление действия.другие связанные классы типов добавляют дополнительную функциональность к стрелке, например, возможность комбинировать стрелки с
Eitherа также(,).
мой любимый пример
Arrowи преобразователи потока с отслеживанием состояния, которые выглядят примерно так:data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))A
StreamTransстрелка преобразует входное значение в выходное и "обновленную" версию себя; рассмотрим способы, которыми это отличается от состоянияMonad.пишу экземпляров
Arrowи связанные с ним классы типов для вышеуказанного типа могут быть хорошим упражнением для понимания того, как они работают!я написал аналогичный ответ ранее что вы можете найти полезным.
Я хотел бы добавить, что стрелки в Haskell намного проще, чем они могут показаться по материалам литературы. Это просто абстракции функций.
чтобы увидеть, как это практически полезно, подумайте, что у вас есть куча функции, которые вы хотите создать, где некоторые из них чисты, а некоторые монадический. Например,
f :: a -> b,g :: b -> m1 cиh :: c -> m2 d.зная каждого из этих типов, я мог бы построить композицию вручную, но тип вывода состав должен был бы отражать промежуточное звено типы монад (в приведенном выше случае,
m1 (m2 d)). Что делать, если я просто хотел лечить функции, как если бы они были простоa -> b,b -> cиc -> d? То есть, Я хочу абстрагироваться от присутствия монад и рассуждать только о базовый тип. Я могу использовать стрелки, чтобы сделать именно это.вот стрелка, которая абстрагирует от присутствия IO для функций в IO монада, такая, что я могу составить их с чистыми функциями без составление кода необходимо знать, что IO участвует. Начнем с определения IOArrow для обертывания функций ввода-вывода:
data IOArrow a b = IOArrow { runIOArrow :: a -> IO b } instance Category IOArrow where id = IOArrow return IOArrow f . IOArrow g = IOArrow $ f <=< g instance Arrow IOArrow where arr f = IOArrow $ return . f first (IOArrow f) = IOArrow $ \(a, c) -> do x <- f a return (x, c)затем я делаю некоторые простые функции, которые я хочу написать:
foo :: Int -> String foo = show bar :: String -> IO Int bar = return . readи использовать их:
main :: IO () main = do let f = arr (++ "!") . arr foo . IOArrow bar . arr id result <- runIOArrow f "123" putStrLn resultздесь я называю IOArrow и runIOArrow, но если бы я передавал эти стрелки вокруг в библиотеке полиморфных функций, им нужно будет только принять аргументы типа "Стрелка a => a b c". Ни один из них код библиотеки должен был бы имейте в виду, что монада была вовлечена. Только создатель и конечный пользователь Эрроу должен знать.
обобщение IOArrow для работы с функциями в любой монаде называется " Kleisli стрелка", и уже есть встроенная стрелка для этого:
main :: IO () main = do let g = arr (++ "!") . arr foo . Kleisli bar . arr id result <- runKleisli g "123" putStrLn resultвы можете, конечно, также использовать операторы композиции стрелок и синтаксис proc, чтобы сделайте это немного яснее, что стрелки участвуют:
arrowUser :: Arrow a => a String String -> a String String arrowUser f = proc x -> do y <- f -< x returnA -< y main :: IO () main = do let h = arr (++ "!") <<< arr foo <<< Kleisli bar <<< arr id result <- runKleisli (arrowUser h) "123" putStrLn resultвот он должен будьте ясны, что хотя
mainзнает, что монада IO участвует,arrowUserнет. Не было бы никакого способа "скрыть" IO отarrowUserбез стрел - не прибегая кunsafePerformIOвключить промежуточное монадическое значение возвращается в чистое (и, таким образом, теряет этот контекст навсегда.) Например:arrowUser' :: (String -> String) -> String -> String arrowUser' f x = f x main' :: IO () main' = do let h = (++ "!") . foo . unsafePerformIO . bar . id result = arrowUser' h "123" putStrLn resultпопробуйте написать это без
unsafePerformIO, а неarrowUser'необходимости справиться с любыми аргументами типа монады.
есть лекции Джона Хьюза из семинара AFP (Advanced Functional Programming). Обратите внимание, что они были написаны до того, как классы Arrow были изменены в базовых библиотеках:
когда я начал изучать композиции стрелок (по существу монады), мой подход состоял в том, чтобы вырваться из функционального синтаксиса и композиции, с которыми он чаще всего связан, и начать с понимания его принципов, используя более декларативный подход. Имея это в виду, я нахожу следующую структуру более понятной:
function(x) { func1result = func1(x) if(func1result == null) { return null } else { func2result = func2(func1result) if(func2result == null) { return null } else { func3(func2result) }так, по существу, для некоторого значения
x, сначала вызовите одну функцию, которая, как мы предполагаем, может вернутьnull(func1), другой, который может ретунnullили быть назначенное наnullвзаимозаменяемо, наконец, третья функция, которая также может возвращатьnull. Теперь задано значениеx, передайте x в func3, только тогда, если он не возвращаетnull, передайте это значение в func2, и только если это значение не равно null, передайте это значение в func1. Он более детерминирован, и поток управления позволяет создавать более сложную обработку исключений.здесь мы можем использовать композицию стрелки:
(func3 <=< func2 <=< func1) x.
Comments