Ожидать любое событие из нескольких событий одновременно в F#

В F# я знаю, как асинхронно ждать одного события, используя Async.AwaitEvent:

let test = async {
  let! move = Async.AwaitEvent(form.MouseMove)
  ...handle move... }

Предположим, я хочу подождать MouseMove или KeyDown событие. Я хотел бы иметь что-то вроде этого:

let! moveOrKeyDown = Async.AwaitEvent(form.MouseMove, form.KeyDown)

Эта функция не существует, но есть ли другой способ сделать это?

4 ответа

Решение
let ignoreEvent e = Event.map ignore e

let merged = Event.merge (ignoreEvent f.KeyDown) (ignoreEvent f.MouseMove)
Async.AwaitEvent merged

РЕДАКТИРОВАТЬ: еще одна версия, которая сохраняет оригинальные типы

let merged = Event.merge (f.KeyDown |> Event.map Choice1Of2) (f.MouseMove |> Event.map Choice2Of2)
Async.AwaitEvent merged

РЕДАКТИРОВАТЬ 2: в соответствии с комментариями Томаса Петричека

let e1 = f.KeyDown |> Observable.map Choice1Of2
let e2 = f.MouseMove |> Observable.map Choice2Of2
let! evt = Observable.merge e1 e2 |> Async.AwaitObservable

ПримитивAwaitObservable можно взять отсюда ("Реактивные демонстрации в Silverlight" Томаса Петричека).

Я использовал реализацию метода, который вы используете в своем примере, в беседе о реактивном программировании, которую я проводил в Лондоне (внизу страницы есть ссылка на скачивание). Если вам интересна эта тема, вы также можете найти полезную беседу:-).

Используемая версия берет IObservable вместо IEvent (поэтому название метода AwaitObservable). Есть некоторые серьезные утечки памяти при использовании Event.merge (и другие комбинаторы из Event модуль) вместе с AwaitEventтак что вы должны использовать Observable.merge и т. д. AwaitObservable вместо.

Проблема описана более подробно здесь (см. Четкий пример в разделе 3). Вкратце - когда вы используете Event.merge, он присоединяет обработчик к исходному событию (например, MouseDown), но он не удаляет обработчик после завершения ожидания с помощью AwaitEvent, поэтому событие никогда не удаляется - если вы продолжаете ждать в цикле, закодированном с использованием асинхронного рабочего процесса, вы продолжаете добавлять новые обработчики (которые ничего не делают при запуске).

Простое правильное решение (основанное на том, что написал desco) будет выглядеть так:

let rec loop () = async {
  let e1 = f.KeyDown |> Observable.map Choice1Of2
  let e2 = f.MouseMove |> Observable.map Choice2Of2
  let! evt = Observable.merge e1 e2 |> Async.AwaitObservable
  // ...
  return! loop() } // Continue looping

Кстати, вы также можете посмотреть эту статью (на основе главы 16 из моей книги).

Чтобы понять, что происходит, я посмотрел исходный код на Event.map, Event.merge и Choice.

type Choice<'T1,'T2> = 
    | Choice1Of2 of 'T1 
    | Choice2Of2 of 'T2

[<CompiledName("Map")>]
let map f (w: IEvent<'Delegate,'T>) =
    let ev = new Event<_>() 
    w.Add(fun x -> ev.Trigger(f x));
    ev.Publish

[<CompiledName("Merge")>]
let merge (w1: IEvent<'Del1,'T>) (w2: IEvent<'Del2,'T>) =
    let ev = new Event<_>() 
    w1.Add(fun x -> ev.Trigger(x));
    w2.Add(fun x -> ev.Trigger(x));
    ev.Publish

Это означает, что наше решение создает 3 новых события.

async {
    let merged = Event.merge 
                     (f.KeyDown |> Event.map Choice1Of2) 
                     (f.MouseMove |> Event.map Choice2Of2)
    let! move = Async.AwaitEvent merged
}

Мы могли бы свести это к одному событию, сделав тесно связанную версию этого библиотечного кода.

type EventChoice<'T1, 'T2> = 
    | EventChoice1Of2 of 'T1
    | EventChoice2Of2 of 'T2
    with 
    static member CreateChoice (w1: IEvent<_,'T1>) (w2: IEvent<_,'T2>) =
        let ev = new Event<_>()
        w1.Add(fun x -> ev.Trigger(EventChoice1Of2 x))
        w2.Add(fun x -> ev.Trigger(EventChoice2Of2 x))
        ev.Publish

И вот наш новый код.

async {
    let merged = EventChoice.CreateChoice form.MouseMove form.KeyDown
    let! move = Async.AwaitEvent merged
}

Вы можете использовать комбинацию Event.map а также Event.merge:

let eventOccurs e = e |> Event.map ignore
let mouseOrKey = Event.merge (eventOccurs frm.MouseMove) (eventOccurs frm.KeyDown)

Тогда вы можете использовать Async.AwaitEvent с этим новым событием. Если MouseMove а также KeyDown был тот же тип, вы могли бы пропустить Event.map шаг и просто напрямую слить их.

РЕДАКТИРОВАТЬ

Но в томас указывает, что вы должны использовать Observable комбинаторы в предпочтении Event из них.

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