Spec-Zone.ru › Haskell 9

6.2.20. Стрелочная нотация

Arrows
Начиная с:

6.8.1

Включает стрелочную нотацию.

Стрелки — это обобщение монад, введённое Джоном Хьюзом. Подробнее см. в следующих работах:

  • «Обобщение монад до стрелок», Джон Хьюз, в журнале Science of Computer Programming 37, стр. 67–111, май 2000 г. Статья, в которой были введены стрелки: доступное введение с примерами программ.
  • «Новая нотация для стрелок», Росс Патерсон, на конференции ICFP, сентябрь 2001 г. В ней введена описанная здесь нотация.
  • «Стрелки и вычисления», Росс Патерсон, в сборнике The Fun of Programming, Palgrave, 2003 г.
  • «Программирование со стрелками», Джон Хьюз, на 5-й международной летней школе по продвинутому функциональному программированию, Lecture Notes in Computer Science, т. 3622, Springer, 2004 г. Эта статья содержит ещё одно введение в нотацию с практическими примерами.
  • «Правила типов и трансляции для стрелочной нотации в GHC», Росс Патерсон и Саймон Пейтон Джонс, 16 сентября 2004 г. Краткий перечень используемых формальных правил (извлечённый из комментариев в исходном коде).
  • Веб-страница о стрелках: https://www.haskell.org/arrows/ <https://www.haskell.org/arrows/>`__.

С расширением Arrows GHC поддерживает стрелочную нотацию, описанную во второй из этих статей, и транслирует её с помощью комбинаторов из модуля Control.Arrow. Далее приводится краткое введение в нотацию; оно будет не слишком понятным, если вы не читали статью Хьюза.

Расширение добавляет новый вид выражения для определения стрелок:

exp10 ::= ...
       |  proc apat -> cmd

где proc — новое ключевое слово. Переменные шаблона связываются в теле выражения proc, которое представляет собой новый вид конструкции, называемый командой. Синтаксис команд следующий:

cmd   ::= exp10 -<  exp
       |  exp10 -<< exp
       |  cmd0

где ⟨cmd⟩0–⟨cmd⟩9 определяются с помощью инфиксных операторов так же, как и для выражений, а

cmd10 ::= \ apat ... apat -> cmd
       |  let decls in cmd
       |  if exp then cmd else cmd
       |  case exp of { calts }
       |  do { cstmt ; ... cstmt ; cmd }
       |  fcmd

fcmd  ::= fcmd aexp
       |  ( cmd )
       |  (| aexp cmd ... cmd |)

cstmt ::= let decls
       |  pat <- cmd
       |  rec { cstmt ; ... cstmt [;] }
       |  cmd

где ⟨calts⟩ аналогичны ⟨alts⟩, за исключением того, что вместо выражений в телах находятся команды.

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

Простой пример новой нотации — выражение

proc x -> f -< x+1

Мы называем это процедурой или абстракцией стрелки. Как и в лямбда-выражении, переменная x — это новая переменная, связываемая внутри выражения proc. Она обозначает вход стрелки. В приведённом выше примере -< — не идентификатор, а новый зарезервированный символ, используемый для построения команды из выражения типа стрелки и выражения, подаваемого на вход этой стрелки. (Странный вид станет понятнее позднее.) Его можно читать как аналог применения функции для стрелок. Приведённый выше пример эквивалентен выражению Haskell

arr (\ x -> x+1) >>> f

Это не имело бы смысла, если бы выражение слева от -< содержало связанную переменную x. В более общем случае выражение слева от -< не может содержать локальные переменные, то есть переменные, связанные в текущей абстракции стрелки. Для таких случаев есть вариант -<<, например:

proc x -> f x -<< x+1

что эквивалентно

arr (\ x -> (f x, x+1)) >>> app

поэтому в данном случае стрелка должна принадлежать классу ArrowApply. Такая стрелка эквивалентна монаде, поэтому, если вы используете эту форму, монадическая формулировка может оказаться удобнее.

6.2.20.1. do-нотация для команд

Ещё одна форма команды — форма do-нотации. Например, можно написать

proc x -> do
        y <- f -< x+1
        g -< 2*y
        let z = x+y
        t <- h -< x*z
        returnA -< t+z

Это можно читать почти так же, как обычную do-нотацию, но вместо монадических выражений здесь используются команды. В первой строке значение x+1 подаётся на вход стрелки f, а её результат сопоставляется с y. В следующей строке результат отбрасывается. Стрелка returnA определена в модуле Control.Arrow как arr id. Приведённый выше пример рассматривается как сокращение для

arr (\ x -> (x, x)) >>>
        first (arr (\ x -> x+1) >>> f) >>>
        arr (\ (y, x) -> (y, (x, y))) >>>
        first (arr (\ y -> 2*y) >>> g) >>>
        arr snd >>>
        arr (\ (x, y) -> let z = x+y in ((x, z), z)) >>>
        first (arr (\ (x, z) -> x*z) >>> h) >>>
        arr (\ (t, z) -> t+z) >>>
        returnA

Обратите внимание, что переменные, которые больше не используются в композиции, исключаются. После упрощения с помощью правил переписывания (см. Правила переписывания), определённых в модуле Control.Arrow, выражение сводится к

arr (\ x -> (x+1, x)) >>>
        first f >>>
        arr (\ (y, x) -> (2*y, (x, y))) >>>
        first g >>>
        arr (\ (_, (x, y)) -> let z = x+y in (x*z, z)) >>>
        first h >>>
        arr (\ (t, z) -> t+z)

именно так вы могли бы написать вручную. При использовании стрелочной нотации GHC отслеживает все эти кортежи переменных за вас.

Обратите внимание: хотя приведённая выше трансляция предполагает, что связанные с let переменные, например z, должны быть мономорфными, фактическая трансляция порождает Core, поэтому полиморфные переменные допустимы.

Также можно определить взаимно рекурсивные привязки с помощью нового ключевого слова rec, как в следующем примере:

counter :: ArrowCircuit a => a Bool Int
counter = proc reset -> do
        rec     output <- returnA -< if reset then 0 else next
                next <- delay 0 -< output+1
        returnA -< output

При трансляции таких форм используется комбинатор loop, поэтому соответствующая стрелка должна принадлежать классу ArrowLoop.

6.2.20.2. Условные команды

В предыдущем примере мы использовали условное выражение для построения входа стрелки. Иногда требуется условно выполнять разные команды, например:

proc (x,y) ->
        if f x y
        then g -< x+1
        else h -< y+2

что транслируется в

arr (\ (x,y) -> if f x y then Left x else Right y) >>>
        (arr (\x -> x+1) >>> g) ||| (arr (\y -> y+2) >>> h)

Поскольку при трансляции используется |||, соответствующая стрелка должна принадлежать классу ArrowChoice.

Также существуют команды case, например:

case input of
    [] -> f -< ()
    [x] -> g -< x+1
    x1:x2:xs -> do
        y <- h -< (x1, x2)
        ys <- k -< xs
        returnA -< y:ys

Синтаксис такой же, как у выражений case, за исключением того, что тела альтернатив содержат команды, а не выражения. Трансляция аналогична трансляции команд if.

6.2.20.3. Определение собственных управляющих конструкций

Как мы видели, стрелочная нотация предоставляет конструкции для последовательного выполнения, рекурсии по значениям и условных выражений, моделируемые по аналогии с конструкциями для выражений. Но для создания новых команд на основе существующих также можно использовать подходящие комбинаторы, определённые в обычном Haskell. Основная идея заключается в том, что команда задаёт стрелку из окружений в значения. Эти окружения сопоставляют значения свободным локальным переменным команды. Поэтому комбинаторы, создающие стрелки из стрелок, также можно использовать для построения команд из команд. Например, класс ArrowPlus включает комбинатор

ArrowPlus a => (<+>) :: a b c -> a b c -> a b c

поэтому его можно использовать для построения команд:

expr' = proc x -> do
                returnA -< x
        <+> do
                symbol Plus -< ()
                y <- term -< ()
                expr' -< x + y
        <+> do
                symbol Minus -< ()
                y <- term -< ()
                expr' -< x - y

(do в первой строке нужен, чтобы первый <+> ... не был интерпретирован как часть выражения в предыдущей строке.) Это эквивалентно

expr' = (proc x -> returnA -< x)
        <+> (proc x -> do
                symbol Plus -< ()
                y <- term -< ()
                expr' -< x + y)
        <+> (proc x -> do
                symbol Minus -< ()
                y <- term -< ()
                expr' -< x - y)

Здесь мы фактически используем <+> с более конкретным типом

ArrowPlus a => (<+>) :: a (e,()) c -> a (e,()) c -> a (e,()) c

Важно, чтобы этот оператор был полиморфен по e (представляющему входное окружение команды и, следовательно, её подкоманд) и удовлетворял соответствующему свойству естественности

arr (first k) >>> (f <+> g) = (arr (first k) >>> f) <+> (arr (first k) >>> g)

по крайней мере для строгого k. (Это должно выполняться автоматически, если вы не используете seq.) Это гарантирует, что окружения, видимые подкомандам, являются окружениями всей команды, а также позволяет безопасно сокращать эти окружения при трансляции. (Вторая компонента входных пар может содержать безымянные входные значения, как описано в следующем разделе.) Оператор также не должен использовать переменные, определённые внутри текущей абстракции стрелки.

Можно определить собственный оператор

untilA :: ArrowChoice a => a (e,s) () -> a (e,s) Bool -> a (e,s) ()
untilA body cond = proc x -> do
        b <- cond -< x
        if b then returnA -< ()
        else do
                body -< x
                untilA body cond -< x

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

proc x -> do
        y <- f -< x+1
        (|untilA (increment -< x+y) (within 0.5 -< x)|)

6.2.20.4. Примитивные конструкции

Некоторым операторам потребуется передавать дополнительные входные данные своим подкомандам. Например, в типе стрелок с поддержкой исключений оператор, присоединяющий обработчик исключений, должен передавать обработчику возникшее исключение. Такой оператор может иметь тип

handleA :: ... => a (e,s) c -> a (e,(Ex,s)) c -> a (e,s) c

где Ex — тип обрабатываемых исключений. Затем его можно использовать со стрелочной нотацией, написав команду

body `handleA` \ ex -> handler

так что, если в команде body возникает исключение, переменной ex присваивается значение исключения и запускается команда handler, которая обычно обращается к ex. Хотя синтаксис здесь похож на функциональную лямбда-абстракцию, речь идёт о командах, и здесь действует иной механизм. Вход стрелки, представленной командой, состоит из значений свободных локальных переменных команды и стека анонимных значений. Во всех предыдущих примерах мы не делали предположений об этом стеке. Во втором аргументе handleA значение исключения добавлено во входной стек обработчика. Форма команды с лямбда-абстракцией лишь присваивает этому значению имя.

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

a (e, (t1, ... (tn, ())...)) t

где ⟨e⟩ — полиморфная переменная (представляющая окружение), а ⟨ti⟩ — типы значений в стеке, причём ⟨t1⟩ находится «на вершине». Полиморфная переменная ⟨e⟩ не должна встречаться в ⟨a⟩, ⟨ti⟩ или ⟨t⟩. Однако используемые стрелки не обязаны быть одинаковыми. Вот ещё несколько примеров подходящих операторов:

bracketA :: ... => a (e,s) b -> a (e,(b,s)) c -> a (e,(c,s)) d -> a (e,s) d
runReader :: ... => a (e,s) c -> a' (e,(State,s)) c
runState :: ... => a (e,s) c -> a' (e,(State,s)) (c,State)

Дополнительный вход, необходимый командам, построенным с помощью двух последних операторов, можно передать, применив их к обычным выражениям, например:

proc x -> do
        s <- ...
        (|runReader (do { ... })|) s

это добавляет s в стек входных данных команды, построенной с помощью runReader.

Формы команд для лямбда-абстракции и применения аналогичны формам для выражений. В частности, правила бета и эта описывают эквивалентности команд. Эти три возможности (операторы, лямбда-абстракция и применение) составляют основу нотации; всё остальное можно построить с их помощью, хотя результат будет несколько громоздким. Например, можно имитировать do-нотацию, определив

bind :: Arrow a => a (e,s) b -> a (e,(b,s)) c -> a (e,s) c
u `bind` f = returnA &&& u >>> f

bind_ :: Arrow a => a (e,s) b -> a (e,s) c -> a (e,s) c
u `bind_` f = u `bind` (arr fst >>> f)

Также можно имитировать if, определив

cond :: ArrowChoice a => a (e,s) b -> a (e,s) b -> a (e,(Bool,s)) b
cond f g = arr (\ (e,(b,s)) -> if b then Left (e,s) else Right (e,s)) >>> f ||| g

6.2.20.5. Отличия от статьи

  • Вместо единой формы применения стрелки (стрелочного хвоста) с двумя вариантами трансляции в реализации предусмотрены две формы: -< (первого порядка) и -<< (высшего порядка).
  • Пользовательские операторы обозначаются банановыми скобками, а не новым ключевым словом form.
  • В статье и предыдущей реализации значения стека объединялись справа от окружения в одном аргументе, а теперь окружение и стек передаются как отдельные аргументы.

6.2.20.6. Переносимость

Хотя стрелочную нотацию напрямую реализует только GHC, существует также препроцессор (доступный на веб-странице о стрелках), который транслирует стрелочную нотацию в Haskell 98 для использования с другими системами Haskell. Программы со стрелками всё равно стоит проверять с помощью GHC: отладить ошибки типов в выходных данных препроцессора непросто. Модули, предназначенные и для GHC, и для препроцессора, должны соблюдать некоторые дополнительные ограничения:

  • Модуль должен импортировать Control.Arrow.
  • Препроцессор не поддерживает другие расширения Haskell. Их необходимо вынести в отдельные модули.
  • Поскольку препроцессор генерирует Haskell (а не Core), связанные с let переменные являются мономорфными.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/exts/arrows.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API