F#でRxる。よく訓練されたF#erはコンピューテーション式をつくる。
いにしえからの言い伝えによると、よく訓練されたF#erはコンピューテーション式をつくるそうです。
Select Many: Reactive Extensions’ Mother Of All Operators (Chaining). お、SelectManyですね。
なるほどなるほど。計算を繋ぐもの。モナドですか。そこでコンピューテーション式(Computation expressions)ですねわかります。
リアクティブプログラミングでリア充
リアクティブプログラミングとは何か。「Reactive Programming」を直訳すると「反応的なプログラミング」です。その名の通り、反応的に作用するようにプログラミングをすることです。よくわかりませんね。
具体的には、値(あるいは状態)を直接的に扱わないで、「時間とともに変化する値(状態)」と「振る舞い(behavior)」の関係性に着目して、
宣言的にプログラムを表現するパラダイム。あるいは、それを軸としたプログラミングスタイルです。
リアクティブプログラミングは、値(状態)やIO、あるいは時間に対する振る舞いをマッピングし、
計算の遅延や合成、差分適用、非同期処理の扱いなどに長けています。(.NETでは、Rx(Reactive Extensions for .NET)を利用するなどで実現できる。)
Functional Reactive Programming(FRP)は、これを関数型の世界に持ってきたものです*1。
リアクティブプログラミングは先進的で、アカデミックな雰囲気が強いので、少しとっつきにくい所もありますが、
理解して使いこなせるようになれば、未来は明るいです。藤本幸世もびっくりのモテキ到来も夢じゃないです(ギークにモテモテ的な意味で)。
Rx(Reactive Extensions for .NET)とは
Rx(Reactive Extensions for .NET)は、LINQ to Objectsの数学的な意味の双対で、LINQシーケンスベースに、非同期イベント処理などのリアクティブな記述を、pushベース と pullベース の両面からサポートするリアクティブフレームワークです。
最近では、「NuGet(ぬげっと)」でお手軽に導入できるようにもなりましたし、Windows Phone 7に標準搭載もされました。
まだまだ国内の情報は少ないのですが、非常に便利で強力なフレームワークなので利用しない手はないです。
Rxの日本語情報に関しては、@neueccさんのブログの記事がもっとも充実していると思います。ステキです。助かります!
@neueccさんのRxカテゴリ - neue.cc
http://neue.cc/category/programming/rx
逆に考えるんだ。IEnumerableがpullなら、IObservableはpush
Rxを理解する最も簡単な方法のひとつは、おそらくIEnumerable以下の例は、短いIEnumerable
var oddNumbers = Enumerable.Range(1, 10).Where(n => n % 2 == 1); foreach (int n in oddNumbers) { Console.WriteLine(n); }
変数oddNumbersへ代入した時点では、まだ計算は評価されません。
foreachによってIEnumerable
foreachが何をしてくれているかというと、実際は以下の糖衣構文です。
var enumerator = Enumerable.Range(1, 10).Where(n => n % 2 == 1).GetEnumerator(); while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); }
MoveNextメソッドの呼び出しは、その都度データを“引いています(pull)”。
IEnumerable
Rxを利用して同じ結果を得るには、例えば以下のように書きます。
var oddNumbers2 = Observable.Range(1, 10).Where(n => n % 2 == 1); oddNumbers2.Subscribe(x => Console.WriteLine(x));
Rxを知らないで見た場合、ぱっと見何が起こったのかわかりませんが、
この方法でも同じ結果が得られます。
では、一体何がおこったのでしょう。
以下は、oddNumbers2がやっていることをイメージしやすい(?)ように、別の方法で実装してみた例です。
(oddNumbers2の糖衣構文ではありませんので注意してください)
var oddNumbers3 = Observable.CreateWithDisposable<int>(observer => { var cancel = new CancellationTokenSource(); foreach (var n in Enumerable.Range(1, 10)) { if (!cancel.Token.IsCancellationRequested) { observer.OnNext(n); continue; } observer.OnCompleted(); break; } return cancel; }); oddNumbers3.Where(n => n % 2 == 1).Subscribe(x => Console.WriteLine(x));
IObservable
ObservableがObserverの OnNextメソッドを呼び出すことは、すなわちIEnumerable
これは、Observableが、OnNextメソッドによって、Observerに対してデータを“押し出し(push)”ています。
また、IObservable
IEnumrable
さて、説明を試みようとしましたが、感覚的にしかわかっていないので、うまく説明できませんorz
難しいことはよくわかりませんが、IObservable
何が良いのかというと、イベントや非同期処理のハンドリングに役立つようです。
例えば、IObserver
これすなわち、事実上のモナドであり、リアクティブプログラミングを可能にしているというわけです。*2
ご存知の方はご存知のとおり、Rxの向こう側には素晴らしい世界が広がっています。が、その学習コストは大きな負担になるかもしれません。
一般的にプログラムを書く場合というのは、プロアクティブ*3なスタイルが主流なので、パラダイムシフトにはそれなりの痛み*4を伴うことが予想されます。
しかしながら、学習コストに見合っただけのリターンがRxには必ずあります。勉強して損はしません。というかしないと損します。
F#でRxる
さて、C#でRxと戯れるのもよいのですが、C#だけRxと仲良しなのはずるい*5。F#でリアクティブプログラミングがしたいんです。Functional Reactive Programming (FRP)がしたいんです。(`・ω・´)
F#では、なんとEvent(イベント)がファーストクラスオブジェクトなのです(C#とは違うのだよC#とは)。
ですので、Rxに頼らずに素のままのF#でも、ある程度リアクティブプログラミングの実践が可能となっています。
ただし、基本的なこと(mapやfilterやmergeなど)はできるものの、できることには限りがあります。
Control.Event モジュール(F#)がサポートする内容は、Rxの充実具合に比べて、どうしても見劣りしてしまいます。
http://msdn.microsoft.com/ja-jp/library/ee340422.aspx
Eventモジュールに貧弱さを感じざるを得ない(Rx充実的な意味で)。 F#的には、いずれPowerPackでなんらかの対策がなされると思われる(たぶん)が、オーバーロード多杉問題ががが(すべてが必要ってわけでもないけど)。無理やり感は否めないが、とりあえず俺俺ラップしとくか。
ということで、「F#でRxる(F#からRxを使う)」ことにしました。Rxも.NETですので、もちろんF#から扱うことができます。
当然そのまま利用することもできますが、F#は関数型のパラダイムを軸とした関数型言語でありますから、
リアクティブプログラミングに関しても、できることならFunctionalに扱いたいんです。それがF#erの性(saga)というものです。
非常に面倒くさいですが、Rxの関数郡*6をF#の関数でラップしてFRP可能にします。パフォーマンスのことは考えません(キリッ
Rx.fs
namespace FSharp module Rx = open System open System.Linq open System.Threading open System.Windows.Threading let asAction f = new System.Action(f) let doNothing = asAction (fun () -> ()) /// Observable.Create let create f = Observable.Create<_>(fun x -> f x doNothing) let create2 f = let subscribe = Func<_,_>(fun x -> Action(f x)) Observable.Create(subscribe) /// Observable.CreateWithDisposable let createWithDisposable f = let subscribe = Func<_,_>(f) Observable.CreateWithDisposable(subscribe) /// Observer.Create let createObserver next error completed = Observer.Create(Action<_>(next)) let createObserver2 next error = Observer.Create(Action<_>(next), Action<exn>(error)) let createObserver3 next completed = Observer.Create(Action<_>(next), Action(completed)) let createObserver4 next error completed = Observer.Create(Action<_>(next), Action<exn>(error), Action(completed)) /// Observable.FromEvent /// F#では、Event<_,_>.Publishがあるから、もしかしていらねんじゃね的な雰囲気もある… let fromEvent (event:IEvent<_,_>) = create (fun x -> event.Add x.OnNext) let fromEvent2<'TEventArgs when 'TEventArgs :> EventArgs> addHandler removeHandler = Observable.FromEvent(Action<EventHandler<'TEventArgs>>(addHandler), Action<EventHandler<'TEventArgs>>(removeHandler)) let fromEvent3<'TEventArgs, 'TDelegate when 'TEventArgs :> EventArgs and 'TDelegate :> MulticastDelegate> conversion addHandler removeHandler = Observable.FromEvent(Func<EventHandler<'TEventArgs>,'TDelegate>(conversion) ,Action<'TDelegate>(addHandler), Action<'TDelegate>(removeHandler)) let fromEvent4 addHandler removeHandler = Observable.FromEvent(Action<EventHandler>(addHandler), Action<EventHandler>(removeHandler)) let fromEvent5 (event:IEvent<_,_>) = Observable.FromEvent(event.AddHandler, event.RemoveHandler) let fromEvent6 (target:obj) name = Observable.FromEvent(target, name) /// Async<_>. から Observableを生成します。 let fromAsync a = { new IObservable<_> with member x.Subscribe(obserber) = if obserber = null then nullArg "IObserver<'T>" let cts = new CancellationTokenSource() let ao = async { try let! r = a obserber.OnNext(r) obserber.OnCompleted() with e -> cts.Cancel () obserber.OnError(e)} Async.StartImmediate(ao, cts.Token) { new IDisposable with member x.Dispose() = let invoked = ref 0 if Interlocked.CompareExchange(invoked, 1, 0) = 0 then cts.Dispose () } } /// Observable.SelectMany let selectMany source (other:'TSource -> IObservable<'TResult>) = Observable.SelectMany(source, other) let selectMany2 f source = Observable.SelectMany(source, Func<_, IObservable<'TResult>>(f)) let selectMany3 f source = Observable.SelectMany(source, Func<_, seq<'TResult>>(f)) let selectMany4 f g source = Observable.SelectMany(source, Func<_,_>(f), Func<_,_,_>(g)) /// Observable.Catch let catch f source = (source:IObservable<'T>).Catch(Func<_,_>(f)) let catch2 first second = (second:IObservable<'T>).Catch((first:IObservable<'T>)) let catch3 source = Observable.Catch(source) /// Observable.Finally let performFinally f source = Observable.Finally(source, fun _ -> f()) /// Observable.While let While f source = Observable.While(Func<_>(f), source) /// Observable.Empty let empty<'T> = Observable.Empty<'T>() /// Observable.Using let using rs ru = Observable.Using(Func<_>(rs), Func<_,_>(ru)); /// Observable.Concat let concat (first:IObservable<'TSource>) second = Observable.Concat(first, second) let concat2 (source:seq<IObservable<'TSource>>) = Observable.Concat(source) let concat3 (source:IObservable<IObservable<'TSource>>) = Observable.Concat(source) let concat4 (source:IObservable<'TSource>) = Observable.Concat(source) /// Observable.Return let oreturn x = Observable.Return(x) let oreturn2 s x = Observable.Return(x, s) /// Observable.ToEnumerable let toEnumerable source = Observable.ToEnumerable(source) (* 激しく長いのでいろいろ省略 *) type IObservable<'T> with member this.Subscribe(next) = subscribe next this member this.Subscribe(next, error) = subscribeWithError next error this member this.Subscribe(next, error, completed) = subscribeAll next error completed this
うへぇ。なげーよ…(なのでいろいろ省略)。※下の方にSkyDriveへのリンクを追加しました。
よく訓練されたF#erはコンピューテーション式をつくる。
さて、いにしえからの言い伝えによると、よく訓練されたF#erはコンピューテーション式をつくるそうです。
コンピューテーション式についてイチから説明すると、とんでもなく長くなる(というかうまく説明できない)ので端折りますが、
@mzpさんのF#プログラマのためのMaybeモナド入門 - みずぴー日記
http://d.hatena.ne.jp/mzp/20110205/monad
あたりを参照すると、いい感じに雰囲気が感じられるかと思います。
冒頭でも書きましたが、Select Many: Reactive Extensions’ Mother Of All Operators (Chaining)ということで、
SelectManyはIObservable
Rx.fsに追加で
open System.Collections.Generic /// Observableコンピューテーション式 type ObservableBuilder() = member this.Bind(x, f) = f |> selectMany x member this.Return(x) = oreturn x member this.ReturnFrom(x) = x member this.TryWith(a,b) = catch a b member this.TryFinally(x, f) = performFinally x f member this.Zero() = empty member this.While(x,f) = While f x member this.Using(f,g) = using f g member this.Delay f = f () member this.Combine(a,b) = concat a b member this.For(inp,f) = inp |> toEnumerable |> For f member this.Yield(x) = oreturn x member this.YieldFrom(x) = x let observable = new ObservableBuilder()
わーい、Observableコンピューテーション式ができたよー!
しんぷるいずべすと!
コンピューテーション式でForを実装するなら、Combineを実装しなきゃなんないし、Combineを実装するならDelayも必要になる。つまりこれ三点セットってことね。んでYieldとYieldFromはおまけなんだけど、実装しておいた方が使う人はうれしいよね。 #fsharp
いくつかのサンプルコード
「F#でRxる」によるFRPなちょっとしたサンプルをいくつか(非同期ワークフローとの組み合わせもあり)。NUnitとFsUnitを使っています。
Test.fs
長すぎるので、こまかいサンプルを省略しました。
namespace FSharp.Rx.UT open System open System.Windows open System.Windows.Controls module Util = open System.ComponentModel open Microsoft.FSharp.Quotations.Patterns type ViewModelBase () = let propertyChanged = DelegateEvent<PropertyChangedEventHandler>() let getPropertyName = function | PropertyGet(expr,pi,_) -> pi.Name | _ -> invalidOp "プロパティ名の取得に失敗" interface INotifyPropertyChanged with [<CLIEvent>] member this.PropertyChanged = propertyChanged.Publish member this.NotifyPropertyChanged propertyName = propertyChanged.Trigger [|this; PropertyChangedEventArgs propertyName|] member this.NotifyPropertyChanged quotation = quotation |> getPropertyName |> this.NotifyPropertyChanged module Tests = open Util open System open System.Windows.Input open System.ComponentModel type SampleViewModel () = inherit ViewModelBase() let mutable text = "" member this.Message with get () = text and set value = text <- value this.NotifyPropertyChanged <@ this.Message @> type browseViewModel (address:string) = inherit ViewModelBase() let mutable _address = "" do _address <- address member this.Address with get () = _address and set value = _address <- value this.NotifyPropertyChanged <@ this.Address @> open System open System.Windows open System.Windows.Input open System.Windows.Controls open System.Windows.Shapes open System.Windows.Media open System.Xaml open System.IO open System.Text open System.Windows.Markup open System.Drawing open System.Windows.Forms open System.Windows.Threading open System.Windows.Media.Animation open Microsoft.FSharp.Core.Operators.Unchecked open System.Linq open System.Reactive open NUnit.Framework open FsUnit open FSharp open FSharp.Rx let parseXAML (xaml : string) = XamlReader.Parse(xaml) [<TestFixture>] type ``FSharp Rx selectMany`` () = [<Test>] member test.``sample async RunSynchronously`` () = let r = ref 0 let _ = async {printfn "%d" 0; return 1} |> Async.RunSynchronously |> fun x -> async {printfn "%d" x; return x + 2} |> Async.RunSynchronously |> fun x -> async {printfn "%d" x; return x + 3} |> Async.RunSynchronously |> fun x -> async {r := x; printfn "%d,%s" x "owata"} |> Async.Start |> ignore // background wait Rx.bgWait (fun _ -> ()) DispatcherPriority.Background |> ignore !r |> should equal 6 [<Test>] member test.``selectMany01 standerd`` () = let r = ref 0 use hoge = Rx.fromAsync (async { printfn "%d" 0; return 1 }) |> Rx.selectMany <| (fun x -> Rx.fromAsync (async { printfn "%d" x; return x + 2 })) |> Rx.selectMany <| (fun x -> Rx.fromAsync (async { printfn "%d" x; return x + 3 })) |> Rx.subscribe (fun x -> r := x; printfn "%d,%s" x "owata") !r |> should equal 6 [<Test>] member test.``selectMany02 Computation Expressions`` () = let r = ref 0 use d = observable { let! x = observable { printfn "%d" 0; return 1 } let! y = observable { printfn "%d" x; return x + 2 } let! z = observable { printfn "%d" y; return y + 3 } return z } |> Rx.subscribe (fun x -> r:=x; printfn "%d,%s" x "owata") !r |> should equal 6 [<Test>] member test.``selectMany03 Computation Expressions from async`` () = let r = ref 0 use d = observable { let! x = Rx.fromAsync (async { printfn "%d" 0; return 1 }) let! y = Rx.fromAsync (async { printfn "%d" x; return x + 2 }) let! z = Rx.fromAsync (async { printfn "%d" y; return y + 3 }) return z } |> Rx.subscribe (fun x -> r:=x; printfn "%d,%s" x "owata") !r |> should equal 6 [<TestFixture>] type ``FSharp Rx Computation Expressions`` () = [<Test>] member test.``For01`` () = let r = ref 0 use d = observable { let x = Rx.fromAsync (async { return 1 }) let y = Rx.fromAsync (async { return 2 }) let z = Rx.fromAsync (async { return 3 }) let a = x.Concat(y).Concat(z) for i in a do printfn "%d" i return! a } |> Rx.subscribe (fun x -> r:=!r+x; printfn "%d,%s" x "owata") !r |> should equal 6 [<Test>] member test.``For02 Yield`` () = let r = ref 0 use d = observable { let! x = Rx.fromAsync (async { printfn "%d" 0; return 1 }) let! y = Rx.fromAsync (async { printfn "%d" x; return x + 2 }) let z = Rx.fromAsync (async { printfn "%d" y; return y + 3}) let! a = z for i in z -> (printfn "%d" i; a) } |> Rx.subscribe (fun x -> r:=x;printfn "%d,%s" x "owata") !r |> should equal 6 [<Test>] member test.``For03 YieldFrom`` () = let r = ref 0 use d = observable { let! x = Rx.fromAsync (async { printfn "%d" 0; return 1 }) let! y = Rx.fromAsync (async { printfn "%d" x; return x + 2 }) let z = Rx.fromAsync (async { printfn "%d" y; return y + 3}) for i in z do printfn "%d" i yield! z } |> Rx.subscribe (fun x -> r:=x;printfn "%d,%s" x "owata") !r |> should equal 6 [<Test>] member test.``For04 YieldFrom`` () = let r = ref 0 use d = observable { let x = Rx.fromAsync (async { return 1 }) let y = Rx.fromAsync (async { return 2 }) let z = Rx.fromAsync (async { return 3 }) let a = x.Concat(y).Concat(z) for i in a do printfn "%d" i yield! a } |> Rx.subscribe (fun x -> r:=!r+x;printfn "%d,%s" x "owata") !r |> should equal (6 * 3) (* 中略 *) [<Test>] [<STAThread>] member test.``WPF01 FSharpCube`` () = let view = let xaml = @"<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:SampleControls='Sample' Title='F# Cube' Background='SkyBlue' Height='400' Width='600' WindowStartupLocation='CenterScreen'> <DockPanel> <Canvas Name='canvas' Background='Transparent'> <Viewport3D Name='ao' ClipToBounds='True' Width='150' Height='150' Canvas.Left='210' Canvas.Top='110'> <Viewport3D.Camera> <PerspectiveCamera x:Name='myPerspectiveCamera' FarPlaneDistance='15' LookDirection='0,0,-1' UpDirection='0,1,0' NearPlaneDistance='1' Position='0,0,2.25' FieldOfView='70' /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup> <DirectionalLight Color='#F9F9F9' Direction='-0.5,-0.5,-0.5' /> <DirectionalLight Color='#F9F9F9' Direction='0.5,-0.5,-0.5' /> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D TriangleIndices='0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35' Normals='0,0,-1 0,0,-1 0,0,-1 0,0,-1 0,0,-1 0,0,-1 0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 0,-1,0 0,-1,0 0,-1,0 0,-1,0 0,-1,0 0,-1,0 1,0,0 1,0,0 1,0,0 1,0,0 1,0,0 1,0,0 0,1,0 0,1,0 0,1,0 0,1,0 0,1,0 0,1,0 -1,0,0 -1,0,0 -1,0,0 -1,0,0 -1,0,0 -1,0,0 ' TextureCoordinates='1,1 1,0 0,0 0,0 0,1 1,1 0,1 1,1 1,0 1,0 0,0 0,1 0,1 1,1 1,0 1,0 0,0 0,1 1,1 1,0 0,0 0,0 0,1 1,1 1,0 0,0 0,1 0,1 1,1 1,0 0,0 0,1 1,1 1,1 1,0 0,0 ' Positions='-0.5,-0.5,-0.5 -0.5,0.5,-0.5 0.5,0.5,-0.5 0.5,0.5,-0.5 0.5,-0.5,-0.5 -0.5,-0.5,-0.5 -0.5,-0.5,0.5 0.5,-0.5,0.5 0.5,0.5,0.5 0.5,0.5,0.5 -0.5,0.5,0.5 -0.5,-0.5,0.5 -0.5,-0.5,-0.5 0.5,-0.5,-0.5 0.5,-0.5,0.5 0.5,-0.5,0.5 -0.5,-0.5,0.5 -0.5,-0.5,-0.5 0.5,-0.5,-0.5 0.5,0.5,-0.5 0.5,0.5,0.5 0.5,0.5,0.5 0.5,-0.5,0.5 0.5,-0.5,-0.5 0.5,0.5,-0.5 -0.5,0.5,-0.5 -0.5,0.5,0.5 -0.5,0.5,0.5 0.5,0.5,0.5 0.5,0.5,-0.5 -0.5,0.5,-0.5 -0.5,-0.5,-0.5 -0.5,-0.5,0.5 -0.5,-0.5,0.5 -0.5,0.5,0.5 -0.5,0.5,-0.5 ' /> </GeometryModel3D.Geometry> <GeometryModel3D.Transform> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D x:Name='FSharpCube' Angle='0' Axis='1 0 1' /> </RotateTransform3D.Rotation> </RotateTransform3D> </GeometryModel3D.Transform> <GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <VisualBrush> <VisualBrush.Visual> <TextBlock Text=' F#' Background='Gold' /> </VisualBrush.Visual> </VisualBrush> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> <Viewport3D.Triggers> <EventTrigger RoutedEvent='Viewport3D.Loaded'> <BeginStoryboard> <Storyboard> <DoubleAnimation Name='anmRotary' Storyboard.TargetName='FSharpCube' Storyboard.TargetProperty='Angle' From='0' To='360' Duration='0:0:2' RepeatBehavior='Forever' /> </Storyboard> </BeginStoryboard> </EventTrigger> </Viewport3D.Triggers> <Viewport3D.RenderTransform> <TranslateTransform X='0' Y='0' /> </Viewport3D.RenderTransform> </Viewport3D> </Canvas> </DockPanel> </Window> " |> parseXAML :?> Window let ao = xaml.FindName("ao") :?> Viewport3D let canvas = xaml.FindName("canvas") :?> Canvas let mouseMove = canvas.MouseMove |> Rx.fromEvent mouseMove |> Rx.map (fun e -> let mutable targetPoint = e.GetPosition(canvas) targetPoint.X <- targetPoint.X - ao.ActualWidth / 2. targetPoint.Y <- targetPoint.Y - ao.ActualHeight / 2. let d = new Duration(TimeSpan.FromMilliseconds(4500.)) let mutable xAnimation = new DoubleAnimation() xAnimation.To <- Nullable targetPoint.X xAnimation.Duration <- d let mutable yAnimation = new DoubleAnimation() yAnimation.To <- Nullable targetPoint.Y yAnimation.Duration <- d ao.BeginAnimation(Canvas.LeftProperty, xAnimation, HandoffBehavior.Compose) ao.BeginAnimation(Canvas.TopProperty, yAnimation, HandoffBehavior.Compose) ) |> Rx.catch (fun e -> printfn "%s" e.Message; Rx.never) |> Rx.subscribe (fun x -> x) |> ignore xaml let main() = (System.Windows.Application()).Run(view) |> ignore main() [<Test>] [<STAThread>] member test.``WPF02 D&D`` () = let view = let xaml = @"<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' Title='F#,Rx Sample' Height='250' Width='250'> <Canvas x:Name='canvas' Background='White'> <Ellipse Fill='Pink' Width='60' Height='60' Canvas.Left='50' Canvas.Top='35' Canvas.ZIndex='1'/> <Ellipse Fill='SkyBlue' Width='60' Height='60' Canvas.Left='75' Canvas.Top='70'/> <TextBlock Canvas.Left='85' Canvas.Top='95' Text='{Binding Message}' /> </Canvas> </Window>" |> parseXAML :?> Window let canvas = xaml.FindName("canvas") :?> Canvas let leftDown = canvas.MouseLeftButtonDown |> Rx.fromEvent let leftUp = canvas.MouseLeftButtonUp |> Rx.fromEvent let mouseMove = canvas.MouseMove |> Rx.fromEvent let mouseLeave = canvas.MouseLeave |> Rx.fromEvent leftDown |> Rx.map (fun e -> (e, mouseMove |> Rx.takeUntil leftUp |> Rx.takeUntil mouseLeave)) |> Rx.filter (fun (e, _) -> e.Source.Equals(canvas) = false) |> Rx.map (fun (e, o) -> let control = e.Source :?> UIElement let location = (Canvas.GetLeft(control), Canvas.GetTop(control)) let pt1 = e.GetPosition(canvas) o |> map (fun e -> let pt2 = e.GetPosition(canvas) (control, location, pt2 - pt1))) |> Rx.mergeAll |> Rx.subscribe (fun (ctl, (left,top), delta) -> Canvas.SetLeft(ctl, left + delta.X) Canvas.SetTop(ctl, top + delta.Y)) |> ignore xaml let main() = view.DataContext <- SampleViewModel(Message="F# love") (System.Windows.Application()).Run(view) |> ignore main() [<Test>] [<STAThread>] member test.``WPF03 Google Map Client`` () = let view = let xaml = @"<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:l='clr-namespace:FSharp.Rx.UT;assembly=FSharp.Rx.UT' Title='Google Map Client' Height='600' Width='800' WindowStartupLocation='CenterScreen'> <Grid Background='#ddd' > <Grid.RowDefinitions> <RowDefinition Height='Auto' /> <RowDefinition Height='*' /> </Grid.RowDefinitions> <Grid Grid.Row='0'> <Grid.ColumnDefinitions> <ColumnDefinition Width='*' /> <ColumnDefinition Width='Auto' /> </Grid.ColumnDefinitions> <TextBox Grid.Column='0' Name='txtSearch' Margin='8,8,4,8' Text='{Binding Address}' /> <Button Grid.Column='1' Name='btnSearch' Width='64' Margin='4,8,4,8'>search</Button> </Grid> <WebBrowser x:Name='browser' Grid.Row='1' Margin='8,0,8,8' /> </Grid> </Window>" |> parseXAML :?> Window let btnSearch = xaml.FindName("btnSearch") :?> System.Windows.Controls.Button let txtSearch = xaml.FindName("txtSearch") :?> System.Windows.Controls.TextBox let browser = xaml.FindName("browser") :?> System.Windows.Controls.WebBrowser browser.Source <- new Uri(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "index.htm")) let btnOnClick = btnSearch.Click |> Rx.fromEvent let txtOnKeyDown = txtSearch.KeyDown |> Rx.fromEvent let search () = browser.InvokeScript("moveMap", txtSearch.Text) btnOnClick |> Rx.map (fun x -> search ()) |> Rx.catch (fun e -> printfn "%s" e.Message; Rx.never) |> Rx.subscribe (fun x -> ()) |> ignore txtOnKeyDown |> Rx.filter (fun e -> e.Key = Key.Enter) |> Rx.map (fun x -> search()) |> Rx.catch (fun e -> printfn "%s" e.Message; Rx.never) |> Rx.subscribe (fun x -> ()) |> ignore xaml let main() = let b = new browseViewModel("旭川駅" ) view.DataContext <- b (System.Windows.Application()).Run(view) |> ignore main() // nunit-gui-runner let main () = NUnit.Gui.AppEntry.Main([|System.Windows.Forms.Application.ExecutablePath|]) |> ignore main ()
最近…、MVVMという宗教がこわいです><
index.htm
<!DOCTYPE html> <!-- saved from url=(0017)http://localhost/ --> <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Gppgle Map</title> <style type="text/css"> html, body { height: 100%; margin:0px; } #mapCanvas { height: 100%; overflow:auto; } </style> <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script> <script type="text/javascript"> //<![CDATA[ var map; var geo; google.maps.event.addDomListener(window, 'load', function () { var mapdiv = document.getElementById("mapCanvas"); var myOptions = { zoom: 17, center: new google.maps.LatLng(43.763781, 142.358084), mapTypeId: google.maps.MapTypeId.ROADMAP, scaleControl: true }; map = new google.maps.Map(mapdiv, myOptions); geo = new google.maps.Geocoder(); }); function moveMap(address) { if (geo) { geo.geocode({ 'address': address }, function (results, status) { map.setCenter(results[0].geometry.location); }); } } //]]> </script> </head> <body> <div id="mapCanvas"></div> </body> </html>
コードが長すぎてポストできなかったので、SkyDriveにupしました。
参照設定とかもろもろについては、よしなにお願いします。
FSharp.Rx.zip
あわせて読みたい
@neueccさんのRxカテゴリ - neue.cchttp://neue.cc/category/programming/rx
なぜリアクティブプログラミングは重要か。- Conceptual Contexture
http://d.hatena.ne.jp/pokarim/20101226
やさしいFunctional reactive programming(概要編)- maoeのブログ
http://d.hatena.ne.jp/maoe/20100109/1263059731
Functional Reactive Programming - NyaRuRuの日記
http://d.hatena.ne.jp/NyaRuRu/20080317/p1
リアクティブプログラミングが世界中のソフトウェア技術者の95%に普及しないと考える理由と今後に対する期待 - おろかな日々
http://d.hatena.ne.jp/Rinta/20110103/p1
Rx + WCF RIA Services = 簡単?? via(非同期プログラミングは辛いよ) - かずきのBlog@Hatena
http://d.hatena.ne.jp/okazuki/20110113/1294922753
F#でRxしてFRPプログラマになってよ!