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

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

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

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

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

4b9b3361

Ответ 1

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

-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))

1) Стрелка представляет собой вычисление от ввода указанного типа к выходу указанного типа. Стрелка typeclass принимает три аргумента типа: тип стрелки, тип ввода и тип вывода. Глядя на экземпляр головы для экземпляров стрелок, мы находим:

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.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

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

Здесь другой оператор, обычно называемый "разветвлением":

-- &&& 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 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. Можно написать функцию count, которая делает то же самое для любого экземпляра ArrowLoop.

Ответ 2

Взглянув на вашу историю на Stack Overflow, я собираюсь предположить, что вам удобно с некоторыми другими классами стандартного типа, особенно с Functor и Monoid, и начните с краткой аналогии с этими.

Единственной операцией на Functor является fmap, которая служит обобщенной версией map в списках. Это в значительной степени целая цель класса типа; он определяет "вещи, которые вы можете отобразить". Таким образом, в некотором смысле Functor представляет собой обобщение этого конкретного аспекта списков.

Операциями для Monoid являются обобщенные версии пустого списка и (++), и он определяет "вещи, которые могут быть объединены ассоциативно, с конкретной вещью, которая имеет значение идентичности". Списки - это самое простое, что подходит для этого описания, а Monoid представляет собой обобщение этого аспекта списков.

Таким же образом, как и выше, операции класса Category являются обобщенными версиями id и (.), и он определяет "вещи, соединяющие два типа в определенном направлении, которые могут быть связаны голова к хвосту". Таким образом, это представляет собой обобщение этого аспекта функций. Заметно не включены в обобщение являются currying или функции приложения.

Класс Arrow строится из Category, но базовая концепция одинаков: Arrow - это вещи, которые составляют подобные функции и имеют "стрелку идентификации", определенную для любого типа. Дополнительные операции, определенные в классе Arrow, просто определяют способ поднять произвольную функцию на Arrow и способ объединить две стрелки "параллельно" как одну стрелку между кортежами.

Итак, первое, о чем нужно помнить, состоит в том, что вырабатывание выражений Arrow - это, по сути, сложная композиция функций. Комбинаторы, такие как (***) и (>>>), предназначены для написания стиля "pointfree", а нотация proc дает возможность назначать временные имена входам и выходам при подключении.

Полезно отметить, что даже если Arrow иногда описывается как "следующий шаг" из Monad s, там действительно нет очень значимых отношений. Для любого Monad вы можете работать со стрелками Kleisli, которые являются просто функциями с типом типа a -> m b. Оператором (<=<) в Control.Monad является композиция стрелки для них. С другой стороны, Arrow не получает вас Monad, если вы также не включите класс ArrowApply. Поэтому нет прямой связи как таковой.

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

Другие связанные классы классов добавляют дополнительную функциональность стрелке, например, возможность комбинировать стрелки с Either, а также (,).


Мой любимый пример Arrow - это преобразователи потока с состоянием, которые выглядят примерно так:

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))

A StreamTrans стрелка преобразует входное значение в вывод и "обновленную" версию; рассмотрите способы, чтобы это отличалось от состояния Monad.

Присвоение экземпляров для Arrow и связанных классов типа для указанного типа может быть хорошим упражнением для понимания того, как они работают!

Я также написал ранее аналогичный ответ, который может оказаться полезным.

Ответ 3

Я хотел бы добавить, что стрелки в 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, но если бы я проходил эти стрелки вокруг в библиотеке полиморфных функций, им нужно было бы только принять аргументы типа "Arrow a = > a b c". Никому из кода библиотеки не потребуется быть осведомленным о том, что монада была вовлечена. Только создатель и конечный пользователь стрелка должна знать.

Обобщение IOArrow для работы для функций в любой Monad называется "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', чтобы обрабатывать любые аргументы типа Монады.

Ответ 4

Есть примечания к лекции Джона Хьюза из семинара AFP (расширенное функциональное программирование). Обратите внимание, что они были написаны до того, как классы Arrow были изменены в базовых библиотеках:

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

Ответ 5

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

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.