Reactive Banana: использовать параметризованный вызов внешнего API

Начиная с предыдущего вопроса здесь: Reactive Banana: как использовать значения из удаленного API и объединять их в потоке событий

Теперь у меня немного другая проблема: как я могу использовать вывод Behaviour в качестве ввода для операции ввода-вывода и, наконец, отобразить результат операции ввода-вывода?

Ниже приведен код из предыдущего ответа, измененный вторым выходом:

import System.Random

type RemoteValue = Int

-- generate a random value within [0, 10)
getRemoteApiValue :: IO RemoteValue
getRemoteApiValue = (`mod` 10) <$> randomIO

getAnotherRemoteApiValue :: AppState -> IO RemoteValue
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state

data AppState = AppState { count :: Int } deriving Show

transformState :: RemoteValue -> AppState -> AppState
transformState v (AppState x) = AppState $ x + v

main :: IO ()
main = start $ do
    f        <- frame [text := "AppState"]
    myButton <- button f [text := "Go"]
    output   <- staticText f []
    output2  <- staticText f []

    set f [layout := minsize (sz 300 200)
                   $ margin 10
                   $ column 5 [widget myButton, widget output, widget output2]]

    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do    
          ebt <- event0 myButton command

          remoteValueB <- fromPoll getRemoteApiValue
          myRemoteValue <- changes remoteValueB

          let
            events = transformState <$> remoteValueB <@ ebt

            coreOfTheApp :: Behavior t AppState
            coreOfTheApp = accumB (AppState 0) events

          sink output [text :== show <$> coreOfTheApp] 

          sink output2 [text :== show <$> reactimate ( getAnotherRemoteApiValue <@> coreOfTheApp)] 

    network <- compile networkDescription    
    actuate network

Как видите, я пытаюсь использовать новое состояние приложения -> getAnotherRemoteApiValue -> show. Но это не работает.

Реально ли это сделать?

ОБНОВЛЕНИЕ На основе приведенных ниже ответов Эрика Аллика и Генриха Апфельмуса у меня есть текущая ситуация с кодом - это работает :):

{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import System.Random
import Graphics.UI.WX hiding (Event, newEvent)
import Reactive.Banana
import Reactive.Banana.WX


data AppState = AppState { count :: Int } deriving Show

initialState :: AppState
initialState = AppState 0

transformState :: RemoteValue -> AppState -> AppState
transformState v (AppState x) = AppState $ x + v

type RemoteValue = Int

main :: IO ()
main = start $ do
    f        <- frame [text := "AppState"]
    myButton <- button f [text := "Go"]
    output1  <- staticText f []
    output2  <- staticText f []

    set f [layout := minsize (sz 300 200)
                   $ margin 10
                   $ column 5 [widget myButton, widget output1, widget output2]]

    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do    
          ebt <- event0 myButton command

          remoteValue1B <- fromPoll getRemoteApiValue

          let remoteValue1E = remoteValue1B <@ ebt

              appStateE = accumE initialState $ transformState <$> remoteValue1E
              appStateB = stepper initialState appStateE

              mapIO' :: (a -> IO b) -> Event t a -> Moment t (Event t b)
              mapIO' ioFunc e1 = do
                  (e2, handler) <- newEvent
                  reactimate $ (\a -> ioFunc a >>= handler) <$> e1
                  return e2

          remoteValue2E <- mapIO' getAnotherRemoteApiValue appStateE

          let remoteValue2B = stepper Nothing $ Just <$> remoteValue2E

          sink output1 [text :== show <$> appStateB] 
          sink output2 [text :== show <$> remoteValue2B] 

    network <- compile networkDescription    
    actuate network

getRemoteApiValue :: IO RemoteValue
getRemoteApiValue = do
  putStrLn "getRemoteApiValue"
  (`mod` 10) <$> randomIO

getAnotherRemoteApiValue :: AppState -> IO RemoteValue
getAnotherRemoteApiValue state = do
  putStrLn $ "getAnotherRemoteApiValue: state = " ++ show state
  return $ count state

person Randomize    schedule 02.10.2015    source источник
comment
просто для протокола: это не работает не является сообщением об ошибке — вы даже не указываете, является ли это ошибкой компиляции или выполнения, или действительно ли это ошибка, или вы просто не видя желаемого поведения.   -  person Erik Kaplun    schedule 03.10.2015
comment
Вы правы извините. В качестве частичного оправдания у меня возникла эта проблема с платформой Haskell и новым MacOsX «El Capitan», поэтому я не могу воспроизвести ошибки :(: stackoverflow.com/questions/32920452/   -  person Randomize    schedule 03.10.2015
comment
что не так с brew install ghc или чем-то еще? у тебя есть доморощенный? если нет, подумайте о том, чтобы попробовать. вы далеко не уйдете, если у вас не установлен Haskell... Кроме того, я рекомендую использовать Stackage/stack поверх необработанного Hackage/cabal, и поэтому Haskell Platform не рекомендуется, потому что Stackage/stack не рекомендует его . Просто используйте Homebrew GHC или позвольте Stack установить собственную локальную версию (версии) GHC, или и то, и другое.   -  person Erik Kaplun    schedule 03.10.2015
comment
да, пробовал с brew, но теперь cabal не работает (некоторые новые ошибки прав доступа, например, для /usr/bin/ar, не найдены версии cabal self и т. д.). На данный момент полный бардак   -  person Randomize    schedule 03.10.2015
comment
было бы разумно ввести аннотацию mapIO с общим типом, то есть mapIO :: (a -> IO b) -> Event t a -> Moment t (Event t b), как в исходном примере Генриха, что лучше, чем ваша текущая мономорфная подпись. (также я исправил ошибку в вашем коде, где вы вызываете mapIO — кажется, вы до сих пор не разобрались с проблемой Haskell+El Capitan :)   -  person Erik Kaplun    schedule 05.10.2015
comment
LOL да Haskell + El Capitan все еще находится в стадии разработки. Я использовал brew для установки cabal/stack (и других инструментов из cabal) + GHC и пытался скомпилировать reactive-banana-wx с новым GHC-7.10.2. Проблема в том, что у cabal-macosx не обновленная версия fgl (‹5.0) в файле cabal, а rbwx использует версию 0.1.0, которая не компилирует cos из-за ошибки catch. Если вы обновите stack.yaml rbwx, чтобы использовать версию 0.2.3 cabal-macosx, ошибка исчезнет, ​​но вы должны сначала изменить файл cabal c-macosx, чтобы использовать правильный fgl. Но похоже, что проект cabal-macosx мертв, поэтому сначала нужно его скомпилировать.   -  person Randomize    schedule 05.10.2015
comment
что это за проект вообще? учусь? школа? хобби? Я вижу, вы оставили весь фиктивный код и putStrLns.   -  person Erik Kaplun    schedule 08.10.2015
comment
это проект обучения/хобби. Во всяком случае, я просто повторно адаптирую код к чему-то другому.   -  person Randomize    schedule 11.10.2015
comment
есть публичное репо?   -  person Erik Kaplun    schedule 11.10.2015
comment
Да, есть публичное репо с двумя ветками: master с текущим старым централизованным глобальным состоянием и веткой temp_backup, которая использует реактивный банан. В любом случае, этот temp_backup все еще находится в стадии разработки, поэтому он может быть вообще не ясен :). Кстати, на данный момент reactive-banana-wx не работает со значками меню инструментов. github.com/danfran/hxkcd   -  person Randomize    schedule 12.10.2015
comment
Вы также оценивали Sodium, Reflex, Netwrie или Yampa?   -  person Erik Kaplun    schedule 12.10.2015
comment
Reactive-Banana — это мой первый FRP с Haskell, но я также начал рассматривать Sodium (хотя он, похоже, снят с производства для Haskell) и Yampa. Два других еще не проверял.   -  person Randomize    schedule 12.10.2015
comment
@ErikAllik Можно ли считать натрий стандартом де-факто для FRP в Haskell?   -  person Randomize    schedule 12.10.2015
comment
Я не знаю; Мне было просто любопытно, потому что, кажется, не так много сравнительной информации.   -  person Erik Kaplun    schedule 12.10.2015
comment
первая глава здесь (бесплатная загрузка) manning.com/books/functional-reactive-programming это дает вам некоторую информацию о реактивном банане, натрии и frp в целом.   -  person Randomize    schedule 12.10.2015


Ответы (2)


Фундаментальная проблема носит концептуальный характер: FRP-события и поведение можно комбинировать только в чистом виде. В принципе невозможно иметь функцию типа, скажем

mapIO' :: (a -> IO b) -> Event a -> Event b

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


На практике иногда может быть полезно выполнять ввод-вывод при объединении событий и поведения. Как указывает @ErikAllik, комбинатор execute может это сделать. В зависимости от характера getAnotherRemoteApiValue это может быть правильным решением, в частности, если эта функция является идемпотентной или выполняет быстрый поиск из местоположения в ОЗУ.

Однако, если вычисление более сложное, то, вероятно, лучше использовать reactimate для выполнения вычисления ввода-вывода. Используя newEvent для создания AddHandler, мы можем дать реализацию функции mapIO':

mapIO' :: (a -> IO b) -> Event a -> MomentIO (Event b)
mapIO' f e1 = do
    (e2, handler) <- newEvent
    reactimate $ (\a -> f a >>= handler) <$> e1
    return e2

Ключевое отличие от чистого комбинатора

fmap :: (a -> b) -> Event a -> Event b

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

Обратите внимание, что execute также гарантирует, что ввод и результат имеют одновременные появления, но накладывает неформальные ограничения на разрешенный ввод-вывод.


С помощью этого трюка объединения reactimate с newEvent аналогичный комбинатор может быть написан аналогичным образом для поведения. Имейте в виду, что набор инструментов из Reactive.Banana.Frameworks подходит только в том случае, если вы имеете дело с действиями ввода-вывода, точный порядок которых обязательно будет неопределенным.


(Чтобы этот ответ был актуальным, я использовал сигнатуры типов из готовящегося к выпуску реактивного банана 1.0. В версии 0.9 сигнатура типа для mapIO'

mapIO' :: Frameworks t => (a -> IO b) -> Event t a -> Moment t (Event t b)

)

person Heinrich Apfelmus    schedule 03.10.2015
comment
Когда вы говорите, что вычисления больше связаны с использованием reactimate, вы имеете в виду продолжительность времени? Поскольку задействован удаленный API, время отклика может быть переменным (миллисекунды, секунды, никогда). Если это то, что вы имели в виду, единственным решением является использование mapIO/reactimate. - person Randomize; 04.10.2015
comment
Ну, это всегда зависит от того, что вы хотите сделать. Одним из критериев является продолжительность времени: она должна быть намного короче, чем обычные временные рамки, в течение которых происходит внешнее событие. Если ваше вычисление подключается к Интернету и ожидает результатов, то это, скорее всего, не подходит для использования с execute, потому что тем временем будут поступать другие события, которые сеть должна обрабатывать, но не может, потому что она зависла. в акции ИО. - person Heinrich Apfelmus; 04.10.2015
comment
@HeinrichApfelmus: основываясь на вашем объяснении, я полагаю, что использовать execute для возможно дорогих звонков не очень хорошая идея, поэтому мой ответ недействителен в контексте этого конкретного вопроса? - person Erik Kaplun; 04.10.2015
comment
@ErikAllik Ага. Справедливости ради, в вопросе это не указывалось. Однако ваше объяснение получения поведения из события по-прежнему полезно. - person Heinrich Apfelmus; 04.10.2015
comment
@HeinrichApfelmus: на данный момент я использую версию 0.9 RR, пожалуйста, посмотрите еще раз на мой вопрос, так как я обновил его для удобства чтения. - person Randomize; 04.10.2015
comment
@Randomize Я добавил сигнатуру типа для 0.9 - person Heinrich Apfelmus; 05.10.2015
comment
@HeinrichApfelmus: не было бы смысла включать mapIO в reactive-banana? IO кажется большим делом, и чем больше шаблонов для него позаботился reactive-banana, и в противном случае трудно сделать правильно и легко ошибиться, учитывая сложные отношения между FRP и IO, тем проще это заключается в создании реальных приложений с библиотекой. Например, вполне вероятно, что я нашел бы все, что вы сказали, методом проб и ошибок после того, как мое приложение FRP начало плохо себя вести, потому что я использовал execute с медленным вводом-выводом. - person Erik Kaplun; 05.10.2015
comment
@HeinrichApfelmus: хорошо, я думаю, это недоразумение. Вы говорите о mapIO правильно? Итак, эта подпись Control.Event.Handler.mapIO :: (a -> IO b) -> AddHandler a -> AddHandler b. Я говорил о reactimate (поэтому я переписал в коде mapIO, который на данный момент должен быть ненужным). - person Randomize; 05.10.2015
comment
@ErikAllik Можете ли вы создать проблему в системе отслеживания проблем? - person Heinrich Apfelmus; 06.10.2015
comment
@Randomize Ах, извините, мой код содержит ошибку, я забыл вызвать функцию handler. Я исправил это в ответе. Кроме того, это функция mapIO, отличная от функции Control.Event.Handler, хотя они служат настолько похожей цели, что я дал им одно и то же имя. В конце поставлю галочку. - person Heinrich Apfelmus; 06.10.2015
comment
@HeinrichApfelmus спасибо. Я еще не пробовал ваш код, но это имеет смысл. Значение из удаленного API переносится из нового события, связывающего обработчик ввода-вывода. В любом случае, моя MapIO подпись немного отличается от вашей. Я предполагаю, что вы продолжаете ссылаться на новую версию API 1.0. - person Randomize; 06.10.2015
comment
@HeinrichApfelmus: концептуально, что отделяет (a -> IO b) -> Event a -> Event b от (a -> IO b) -> Event a -> MomentIO (Event b) в том, что последний может существовать, а первый нет? Это должно быть что-то очевидное, но ваша формулировка, вероятно, поможет. - person Erik Kaplun; 06.10.2015
comment
@Randomize Вам не нужно было добавлять ограничение Frameworks t, потому что переменная типа t уже находилась в области действия этого ограничения. Если вы сделаете mapIO' определением верхнего уровня, тогда нужно будет добавить ограничение. - person Heinrich Apfelmus; 08.10.2015
comment
@ ЭрикАллик Да. Обратите внимание, что в последнем вход Event a и выход Event b не будут одновременными. По сути, это основная причина, по которой первое не может существовать. - person Heinrich Apfelmus; 08.10.2015
comment
Итак, упаковка MomentIO — это то, что делает Event b неодновременным с Event a? Или я думаю, что я должен прочитать источник или что-то в этом роде... - person Erik Kaplun; 08.10.2015
comment
@ErikAllik Не обязательно. Тип MomentIO — это всего лишь указание на то, что что-то происходит с вводом-выводом, а это значит, что для понимания того, что делает программа, нужно учитывать порядок выполнения. В этом случае результатом является отсутствие одновременности. - person Heinrich Apfelmus; 09.10.2015

TL;DR: прокрутите вниз до раздела ОТВЕТ:, чтобы найти решение и объяснение.


Прежде всего

getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state

недействителен (т.е. не проверяет тип) по причинам, совершенно не связанным с FRP или реактивным бананом: вы не можете добавить Int к IO Int — так же, как вы не можете напрямую применить mod 10 к IO Int, именно поэтому, в ответ на ваш первоначальный вопрос, я использовал <$> (это другое имя для fmap от Functor).

Я настоятельно рекомендую вам изучить и понять назначение/значение <$>, а также <*> и некоторых других методов классов типа Functor и Applicative — FRP (по крайней мере, так, как он разработан в реактивном банане) в значительной степени основан на функторах и аппликативах (и иногда Monads, Arrows и, возможно, какой-то другой более новый фундамент), поэтому, если вы не полностью их понимаете, вы никогда не станете опытным в FRP.

Во-вторых, я не понимаю, почему вы используете coreOfTheApp вместо sink output2 ... — значение coreOfTheApp связано с другим значением API.

В-третьих, как должно отображаться другое значение API? Или, точнее, когда он должен отображаться? Ваше первое значение API отображается при нажатии кнопки, но кнопки для второго нет. Хотите, чтобы та же кнопка вызывала вызов API и отображала обновление? Хотите еще одну кнопку? Или вы хотите, чтобы он опрашивался каждые n единиц времени и просто автоматически обновлялся в пользовательском интерфейсе?

Наконец, reactimate предназначен для преобразования действия Behavior в действие IO, а это не то, что вам нужно, потому что у вас уже есть помощник show и вам не нужно setText или что-то еще на статической метке. Другими словами, то, что вам нужно для второго значения API, такое же, как и раньше, за исключением того, что вам нужно передать что-то из состояния приложения вместе с запросом во внешний API, но помимо этой разницы, вы все равно можете просто продолжать показывать (другое) Значение API с использованием show как обычно.


ОТВЕТ:

Что касается того, как преобразовать getAnotherRemoteApiValue :: AppState -> IO RemoteValue в Event t Int, похожий на исходный remoteValueE:

Сначала я пытался пройти через IORefs и использовать changes+reactimate', но это быстро зашло в тупик (помимо того, что было некрасиво и чрезмерно сложно): output2 всегда обновлялся на один «цикл» FRP слишком поздно, поэтому это всегда была одна «версия». " позади в пользовательском интерфейсе.

Затем я с помощью Oliver Charles (ocharles) из #haskell-game на FreeNode обратился к execute:

execute :: Event t (FrameworksMoment a) -> Moment t (Event t a)

который я до сих пор еще не полностью понял, но он работает:

let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
             (appStateB <@ ebt)
remoteValue2E <- execute x

поэтому одна и та же кнопка будет вызывать оба действия. Но проблема с этим быстро оказалась такой же, как и с решением на основе IORef — поскольку одна и та же кнопка вызывала пару событий, а одно событие внутри этой пары зависело от другого, содержимое output2 все еще отставало на одну версию. .

Затем я понял, что события, связанные с output2, должны запускаться после любых событий, связанных с output1. Однако перейти с Behavior t a -> Event t a невозможно; другими словами, когда у вас есть поведение, вы не можете (легко?) получить из него событие (за исключением changes, но changes привязано к reactimate/reactimate', что здесь бесполезно).

Я, наконец, заметил, что по сути «выбрасываю» промежуточное значение Event в этой строке:

appStateB = accumB initialState $ transformState <$> remoteValue1E

заменив его на

appStateE = accumE initialState $ transformState <$> remoteValue1E
appStateB = stepper initialState -- there seems to be no way to eliminate the initialState duplication but that's fine

так что у меня все еще был точно такой же appStateB, который использовался как и раньше, но я мог также полагаться на appStateE для надежного запуска дальнейших событий, которые зависят от AppState:

let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
             appStateE
remoteValue2E <- execute x

Последняя строка sink output2 выглядит так:

sink output2 [text :== show <$> remoteValue2B] 

Весь код можно увидеть по адресу http://lpaste.net/142202, при этом вывод отладки по-прежнему включен.

Обратите внимание, что лямбда (\s -> FrameworkMoment $ liftIO $ getAnotherRemoteApiValue s) не может быть преобразована в бесточечный стиль по причинам, связанным с типами RankN. Мне сказали, что эта проблема исчезнет в reactive-banana 1.0, потому что не будет вспомогательного типа FrameworkMoment.

person Erik Kaplun    schedule 03.10.2015
comment
На данный момент я не могу ничего скомпилировать, поэтому я пытаюсь объяснить, что я пытался сделать: после нажатия кнопки у вас есть поведение coreOfTheApp, которое в первом выводе просто показывает вам текущий счет. Во втором выводе вы передаете содержимое этого поведения другой удаленной функции ввода-вывода в виде параметра, чтобы получить другой вывод для отображения. - person Randomize; 03.10.2015
comment
mapIO/Handler должен быть тем, что мне нужно: hackage.haskell.org/package/reactive-banana-0.9.0.0/docs/. Потоковое событие в функции IO (вместо Behavior). - person Randomize; 03.10.2015
comment
Хорошо, спасибо за объяснение — по сути, одна и та же кнопка должна запускать оба вызова API; просто новый (очевидно) должен случиться с исходным, потому что исходный обновляет состояние, а новый полагается на состояние при выполнении вызова API. Я также посмотрю на mapIO/Handler. - person Erik Kaplun; 03.10.2015
comment
Честно говоря, я не могу понять, как достичь (Behavior t a, a -> IO b) -> Behavior t b или (Event t a, a -> IO b) -> Event t b или эквивалента... по крайней мере, не с Reactive Banana — кажется, что у Sodium и Reflex есть более полезные функции для получения чего-то подобного, но не у Reactive Banana. - person Erik Kaplun; 03.10.2015
comment
Другими словами, у Sodium есть executeAsyncIO :: Event r (IO a) -> Event r a (и его синхронизированная версия), но у Reactive Banana, похоже, нет ничего подобного. Я пытаюсь выяснить, смогу ли я добиться того же, используя комбинацию changes и reactimate или что-то подобное. - person Erik Kaplun; 03.10.2015