Что такое стрелы, и как я могу их использовать?



Я пытался узнать смысл стрелки, но я их не понял.



Я использовал учебник Wikibooks. Я думаю, что проблема Wikibook в основном заключается в том, что она, похоже, написана для кого-то, кто уже понимает эту тему.



может кто-нибудь объяснить, что такое стрелки и как я могу их использовать?

547   5  

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 . 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 в качестве входных данных и возвращает список целых чисел от 1 до n.

обратите внимание, что мы до сих пор не использовали никаких функций "стрелка", то есть либо методы класса Arrow, либо функции, написанные в их терминах.

4) Большая часть кода выше специфична для каждого экземпляра стрелки[1]. Все в Control.ArrowControl.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

The >>> функция принимает две стрелки и использует выход первой в качестве входа для второй.

вот еще один оператор, обычно называемый "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 были изменены в базовых библиотеках:

http://www.cse.chalmers.se/~rjmh / afp-arrows. pdf

когда я начал изучать композиции стрелок (по существу монады), мой подход состоял в том, чтобы вырваться из функционального синтаксиса и композиции, с которыми он чаще всего связан, и начать с понимания его принципов, используя более декларативный подход. Имея это в виду, я нахожу следующую структуру более понятной:

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

    Ничего не найдено.