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 -> показать. Но это не работает.

Возможно ли это сделать?

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

{-# 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

2 ответа

Решение

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

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

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


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

Однако, если вычисления более сложны, то, вероятно, лучше использовать reactimate выполнить вычисление IO. С помощью 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)

)

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


Прежде всего

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

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

Я настоятельно рекомендую вам посмотреть и понять цель / значение <$>, вместе с <*> и некоторые другие Functor а также Applicative методы класса типов - FRP (по крайней мере, так, как он разработан в реактивном банане) в значительной степени опирается на Функторы и Аппликативы (а иногда и на Монады, Стрелы и, возможно, на какой-то другой более новый фундамент), следовательно, если вы не до конца понимаете их, вы никогда не станет опытным с 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:

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

Затем я с помощью Оливера Чарльза (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. Мне сказали, что эта проблема исчезнет в реактивном банане 1.0, потому что не будет FrameworkMoment вспомогательный тип.

Другие вопросы по тегам