Отказ от ответственности: мне не удалось найти каких-либо крупномасштабных программ, использующих Netwire, поэтому все, что я собираюсь написать, вы должны воспринимать с долей скептицизма, так как это основано на моем собственном опыте использования Netwire. Используемые здесь примеры в основном взяты из моей собственной библиотеки и попытка написать игру с использованием FRP и может быть "неправильным способом" что-то делать.
Вопрос 1: базовая структура (например, как в реактивном банане вы активируете сетевые описания, которые потребляют обработчики, определяют поведение и реагируют на события).
Сеансы. Автор библиотеки netwire дал действительно хороший ответ. о базовой структуре сетевой программы. Поскольку он немного устарел, я обрисую здесь некоторые моменты. Прежде чем мы рассмотрим провода, давайте сначала посмотрим, как netwire обрабатывает время, лежащий в основе драйвера FRP. Единственный способ ускорить время без использования тестовой системы testWire
— создать Session
, который будет возвращать временные дельты с сохранением состояния. Способ Sessions
сохранения состояния инкапсулирован в их тип:
newtype Session m s = Session { stepSession :: m (s, Session m s) }
Здесь Session
находится внутри монады (обычно IO
) и каждый раз, когда она оценивается, возвращает значение "временного состояния" типа s
и новое Session
. Обычно любое полезное состояние s
может быть записано как значение Timed
, которое может возвращать некоторый экземпляр Real t
:
data Timed t s
class (Monoid s, Real t) => HasTime t s | s -> t where
-- | Extract the current time delta.
dtime :: s -> t
instance (Monoid s, Real t) => HasTime t (Timed t s)
Например, в играх вы обычно требуете фиксированный временной шаг для выполнения вашего обновить звонки. netwire кодирует это понятие с помощью:
countSession_ :: Applicative m => t -> Session m (Timed t ())
countSession_
принимает в качестве входных данных временной шаг, в данном случае фиксированное значение типа t
, и создает Session
, значения состояния которого имеют тип Timed t ()
. Это означает, что они кодируют только одно значение типа t
и не содержат никакого дополнительного состояния со значением ()
. После того, как мы обсудим провода, мы увидим, как это влияет на их оценку.
Провода. Основной тип "провода" в Netwire:
Wire s e m a b
Этот провод описывает значение reactive типа b
, которое выполняет следующие действия:
- Принимает на вход реактивное значение типа
a
- Действует внутри Монады
m
- Может запрещать или не создавать значение, давая значение запрета типа
e
- Принимает состояние времени, заданное
s
В силу того, что провода являются реактивными величинами, их можно рассматривать как функции, изменяющиеся во времени. Следовательно, каждый проводник кодируется как функция времени (или состояния времени s
), что создает в данный момент времени новое значение типа b
и новый проводник для оценки следующего ввода типа a
. Возвращая значение и новый проводник, функция может охватывать состояние, распространяя его через определения функций.
Кроме того, проводники могут запрещать или не создавать значение. Это полезно, когда вычисления не определены (например, когда мышь находится за пределами окна приложения). Это позволяет вам реализовать такие вещи, как switch
, где провод меняется на другой провод, чтобы продолжить выполнение (например, игрок завершает свой прыжок).
С этими идеями мы можем увидеть основной драйвер проводов в netwire:
stepWire :: Monad m => Wire s e m a b -> s -> Either e a -> m (Either e b, Wire s e m a b)
stepWire wire timestate input
делает именно то, что мы сказали ранее: он принимает wire
и передает ему текущие timestate
и input
из предыдущего провода. Затем в базовой монаде m
он либо выдает значение Right b
, либо запрещает значение Left e
, а затем дает следующий провод для использования в вычислениях.
Вопрос 2: Как это в конечном итоге происходит в main.
Вооружившись значениями типа Session
и Wire
, мы можем построить цикл, который снова и снова делает две вещи:
- Шаги сеанса для получения нового состояния времени
- Использует новое состояние времени для шага по проводу
Вот пример программы, которая навсегда изменяет фиксированный счетчик, чтобы считать двойками:
import Control.Wire
-- My countLoop operates in the IO monad and takes two parameters:
-- 1. A session that returns time states of type (Timed Int ())
-- 2. A wire that ignores its input and returns an Int
countLoop :: Session IO (Timed Int ()) -> Wire (Timed Int ()) () IO a Int -> IO ()
countLoop session wire = do
(st, nextSession) <- stepSession session
(Right count, nextWire) <- stepWire wire st (Right undefined)
print count
countLoop nextSession nextWire
-- Main just initializes the procedure:
main :: IO ()
main = countLoop (countSession_ 1) $ time >>> (mkSF_ (*2))
Вопрос 3: Как обрабатывать события ввода-вывода (например, щелчок мышью, нажатие клавиши или обратный вызов игрового цикла), как события поступают в сеансы и т. д.
Существует некоторая дискуссия о том, как это сделать. Я думаю, что в этой ситуации лучше всего воспользоваться базовой монадой m
и просто передать моментальный снимок текущего состояния функции stepWire
. При этом большинство моих входных проводов выглядят примерно так:
mousePos :: Wire s e (State Input) a (Float, Float)
Где ввод в провод игнорируется, а ввод мыши читается из монады State
. Я использую State
, а не Reader
, чтобы правильно обрабатывать отскоки клавиш (чтобы нажатие на пользовательский интерфейс также не нажимало на что-то под пользовательским интерфейсом). Состояние задается в моей функции main
и передается в runState
, которая также выполняет шаг проводов. Запрещающее поведение проводов, подобных этому, может стать элегантным кодом. Например, предположим, что у вас есть провода right
и left
для клавиш со стрелками, которые выдают значение, если клавиша нажата, и блокируют в противном случае. Вы можете создать движение персонажа с помощью провода, который выглядит так:
(right >>> moveRight) <|> (left >>> moveLeft) <|> stayPut
Поскольку провода являются экземпляром Alternative
, если right
запрещает, он просто перейдет к следующему возможному проводу. a <|> b
заблокирует только в том случае, если оба a
и b
заблокируют.
Вы также можете написать свой код, чтобы воспользоваться преимуществами системы Event
netwire, но вам придется создавать свои собственные проводники, которые возвращают Event
, используя Control.Wire.Unsafe.Event
. При этом я еще не нашел эту абстракцию более полезной, чем простое торможение.
person
Mokosha
schedule
16.08.2014