F# と Sodium でリアクティブ・プログラミング(2)
Sodium のCell
とアキュームレータの作成
Sodium のStream
が値を持つのはイベント発生の瞬間であるのに対して、Cell
はいったん獲得した値を保持できる。Cell
の値は書き換え可能で、次の書き換えまでその値を保持しており、いつでも自由に読み出すことができる。
本記事は SodiumFRP.FSharp を使用する。
以下に示すサンプルコードの動作は、
- コンソール画面で、キー入力を待つ
- 入力した文字をコンソール画面に出力する。ただし、数字とアルファベット以外のキー入力は無視する。
- 入力が数字キーならば、数字キーが累積で何回押下されたかをコンソール出力する。
- 入力がアルファベットキーならば、アルファベットキーが累積で何回押下されたかをコンソール画面に出力する。
- Esc キーを押せば、入力受付をやめてアプリケーションを終了する。
サンプルコード
サンプルコードの解説
/// キー入力イベント用ストリーム let sKeyInput: StreamSink<ConsoleKeyInfo> = StreamSink.create()
7行目:キー入力イベントを流すStreamSink<ConsoleKeyInfo>
としてsKeyInput
を作成(StreamSink
はStream
型の派生クラス)。StreamSink
型はsendS
関数(49行目で使用)を使って明示的にイベントを送信できる。
use lKeyInput: IStrongListener = sKeyInput |> mapS (fun (ki: ConsoleKeyInfo) -> match ki.KeyChar with | c when Char.IsNumber c -> Some c | c when Char.IsLetter c -> Some c | _ -> None) |> filterOptionS |> listenS (fun (c: char) -> printfn $" %c{c}")
9~17行目:キー入力イベント用ストリームsKeyInput
(StreamSink<ConsoleKeyInfo>
型)を、mapS
関数とfilterOptionS
関数を使ってStream<char>
に変換する。また、数字・アルファベット以外のキー入力イベントを破棄する。
更にlistenS
を使ってリスナー(IStrongListener
型)を作成(17行目)、イベントデータ処理用のリスナー関数を登録する。このリスナー関数では入力文字をそのままコンソール出力する。
/// 数字キーを押した回数を保持するセル let cNumberCounter: Cell<int> = loopWithNoCapturesC (fun (lpcCount: LoopedCell<int>) -> sKeyInput |> snapshotC lpcCount (fun (ki: ConsoleKeyInfo) (acc: int) -> if Char.IsNumber ki.KeyChar then acc + 1 else acc) |> calmS |> holdS 0)
20~26行目:loopWithNoCapturesC
関数を使って、数字キー押下の累積回数を保持するアキュームレータとなるcNumberCounter
(Cell<int>
型)を作成する。ストリームであるsKeyInput
にてイベントが発生する毎に、lpcCount
(LoopedCell<int>
型)の値を読み取り、数字キー入力と判定された場合のみカウンタ値をインクリメントして保持している。(LoopedCell
はCell
型の派生クラス)
calmS
関数を使うことにより、Stream
で発生したイベントの値が直前イベントと重複した場合にはイベントを破棄している。
/// アルファベットキーを押した回数を保持するセル let cLetterCounter: Cell<int> = sKeyInput |> accumS 0 (fun (ki: ConsoleKeyInfo) (acc: int) -> if Char.IsLetter ki.KeyChar then acc + 1 else acc) |> calmC
34~38行目:accumS
関数を使って、アルファベットキー押下の累積回数を保持するアキュームレータとなるcLetterCounter
(Cell<int>
型)を作成する。このとき、calmC
関数を使うことにより、Cell
の値の更新が発生しても既存の値から変化しないときはリスナー呼び出しを行わないように設定できる。
SodiumFRP.FSharp においては、アキュームレータを実現する方法は複数用意されている。本記事のサンプルコードのように、処理が単純なアキュームレータならば、21行目のloopWithNoCapturesC
を使うよりもaccumS
関数の方が簡潔な記述ができる。
※ソースコードはGitHubで公開しています
《参考資料》
F# と Sodium でリアクティブ・プログラミング(1)
F# 向けのリアクティブ・ブログラミングには SodiumFRP.FSharp パッケージが利用できます。
Sodium においては、GUI のボタンクリックイベントやキーボードによるキー入力など何であれ、そういったイベント群をストリーム(Stream<'a>
型)上に並べたデータ群として扱います。
SodiumFRP.FSharp では、イベント処理を、静的な F# のシーケンスやリストに対する処理と同等に扱うコードが書けます。F# のパイプライン演算子も利用できるので便利です。
イベント処理結果の I/O 出力は、 Sodium ではストリームへ登録したリスナーから行います。
簡単な例として、キーボードから入力したキーコードが数字かアルファベットかを判定して、判定結果を文字列としてコンソールに出力するコードを書いてみます。(数字とアルファベット以外のキー入力は無視します。 Esc キーを押せばアプリケーションを終了します。)
ソースコード
6行目:イベント送信可能なストリームとしてStreamSink<ConsoleKeyInfo>
を作成します。(StreamSink<'a>
はStream<'a>
の派生型です。)
7~16行目:ConsoleKeyInfo
型のデータのうち、数字とアルファベット入力だけを有効なイベントとして選抜し、イベントが運搬するデータ(ConsoleKeyInfo
型)を表示用文字列(string
型)に変換する新たなストリーム(Stream<'string>
型)を作成します。
Stream<string option>
に対する filterOptional
のフィルタ処理(16行目)ではNone
以外の値を選抜し、string option
からstring
へ型変換してイベントを通過させています。
18~20行目:sStrOutput
用のリスナー(IStrongListener
型)を作成します。listenS
で、イベントデータを受け取って処理する関数を登録します。(ここでは文字列をコンソール出力する関数を登録しています)
22~24行目:キー入力イベント群を表す無限長のシーケンス(ConsoleKeyInfo seq
型)を作成しています。Escキー入力が来るまで無限にキー入力を受け入れます。
24行目のsendS
関数によってイベントを発生させてConsoleKeyInfo
型のデータを送信しています。
26行目:20行目でsStrOutput
へ登録したリスナーを登録解除します。
※ソースコードはGitHubで公開しています
●概念図
《参考資料》
F# クラスのプロパティ定義をシンプルに記述する
メンバとして public
なプロパティ Name
(string
型、読み書き可)を持つ単純なクラス Person1
を C# と F# 双方で定義してみます。
- [注意] F# のクラスのアクセシビリティはデフォルトで
public
です。同様に、メソッドやプロパティもアクセシビリティはデフォルトでpublic
です。ただしクラス内でlet
束縛された値はprivate
です。
C# と F# でクラス宣言のコード比較
public class Person1 { private string _name = "Tom"; public string Name { get { return _name; } set { _name = value; } } }
type Person1() = let mutable _name = "Tom" member __.Name with get(): string = _name and set (v: string) = _name <- v
C# では自動実装プロパティ(Auto-Implemented Properties)の文法があるので上記のようなプロパティの定義を意味を変えずに以下のようにシンプルに記述できます。
class Person2 { public string Name { get; set; } = "John"; }
F# でも同様にシンプルな記述に変えることができます。
type Person2() = member val Name: string = "John" with get, set
ここで、プロパティの getter / setter のアクセシビリティをそれぞれ別のものを指定する(以下のコードでは getter は public
に、 setter は private
にそれぞれ指定)場合を考えてみます。
C# では以下のようにシンプルに記述できますが、
public class Person3 { public string Name { get; private set; } = "Alice"; }
こういう場合、F# ではどうしても冗長な書き方をするしかありませんが、 getter / setter それぞれのアクセシビリティを個別に指定することはできます。
type Person3() = let mutable _name = "Alice" member __.Name with get(): string = _name and private set (v: string) = _name <- v
また、プロパティの getter / setter は以下のように分けて定義することもできます。
type Person3'() = let mutable _name = "Jane" member __.Name with get(): string = _name member __.Name with private set (v: string) = _name <- v
煩雑な条件分岐を F# のコンピューテーション式で簡潔に表現する
条件分岐の多い処理を、F# の Option 型とコンピューテーション式の文法を活用してコードの可読性を向上させる例を示します。
「関数プログラミング実践入門 ──簡潔で、正しいコードを書くために」《第5章 モナド / 5.4 他の言語におけるモナド》に載っている Java や Haskell のコード片を参考に、 F# のコードで同様の動作をするように独自に解釈して実装を試みています*1
F#で実装したコード
- open System
- type Ticket = FilmTicket
- type Money = int
- type Wallet = { mutable Amount: Money }
- with
- member this.getMoney (cost: Money) =
- if this.Amount >= cost then
- this.Amount <- this.Amount - cost
- printfn " 財布から%A円出す 残金=%A円" cost this.Amount
- Some cost
- else
- printfn " お金が足りない"
- None
- type Pocket = { Wallet: Wallet option; Ticket: Ticket option }
- with
- member this.getWallet(): Wallet option =
- this.Wallet
- member this.getTicket(): Ticket option =
- this.Ticket
- type Person = { Name: string; Pocket: Pocket option }
- with
- member this.getWallet(): Wallet option =
- Option.bind (fun x -> x.Wallet) this.Pocket
- member this.getPocket(): Pocket option =
- this.Pocket
- /// Option型の型拡張:JavaのOptional型と同名・同機能のメソッドを追加
- /// オリジナルのJavaコードに外観を似せるための措置
- type Option<'T> with
- member this.get() = this |> Option.get
- member this.isPresent() = this |> Option.isSome
- member this.flatMap(f) = this |> Option.bind f
- member this.ifPresent(f) = this |> Option.iter f
- type Optional<'T> = Option<'T> // 型のエイリアス
- type Persons() =
- let personDB = seq [
- {Name = "Alice";
- Pocket = Some {Wallet = {Amount = 10000} |> Some; Ticket = FilmTicket |> Some }}
- {Name = "Becky";
- Pocket = Some {Wallet = {Amount = 20000} |> Some; Ticket = None(* 引換券なし *)}}
- {Name = "Cathy";
- Pocket = Some {Wallet = {Amount = 1000} |> Some; Ticket = FilmTicket |> Some}}
- ]
- let findBase (db: Person seq) (name: string): Person option =
- Seq.tryFind (fun x -> x.Name = name) db
- |> fun person ->
- if person.IsNone then
- printfn "該当する名前の人物が存在しません: %A" name
- person
- member __.find = findBase personDB
- /// お金を払う(その1)
- let pay1 (money: Money): unit =
- printfn " pay1 実行 Money= %A" money
- /// お金を払う(その2)
- let pay2 (ticket: Ticket, money: Money): unit =
- printfn " pay2 実行 Ticket= %A, Money= %A" ticket money
- //--------------------------------------------------------------------
- (* 参考資料: 関数プログラミング実践入門 [技術評論社]
- 第5章 モナド / 5.4 他の言語におけるモナド / Optionalクラス p.264 *)
- /// 関数[1-1] JavaのOptional型を模したコード
- let simulateJavaOptional_1(personName: string): unit =
- printfn "\n 関数[1-1] simulateJavaOptional_1 を実行..."
- let persons = new Persons()
- let person: Optional<Person> = persons.find(personName)
- if person.isPresent() then
- let wallet: Optional<Wallet> = person.get().getWallet()
- if wallet.isPresent() then
- let money: Optional<Money> = wallet.get().getMoney(10000)
- if money.isPresent() then
- pay1(money.get())
- /// 関数[1-2] Javaのメソッドチェーンを模したコード(その1)
- let simulateJavaMethodChain_1(personName: string): unit =
- printfn "\n 関数[1-2] simulateJavaMethodChain_1 を実行..."
- let persons = new Persons()
- persons
- .find(personName)
- .flatMap(fun person -> person.getWallet())
- .flatMap(fun wallet -> wallet.getMoney(10000))
- .ifPresent(fun money -> pay1(money))
- /// 関数[1-3] F# のパイプライン演算を利用したコード(その1)
- let usePipeline_1(personName: string): unit =
- printfn "\n 関数[1-3] usePipeline_1 を実行..."
- let persons = new Persons()
- persons.find(personName)
- |> Option.bind(fun person -> person.getWallet())
- |> Option.bind(fun wallet -> wallet.getMoney(10000))
- |> Option.iter(fun money -> pay1(money))
- (* 参考資料: 関数プログラミング実践入門 [技術評論社]
- 第5章 モナド / 5.4 他の言語におけるモナド / メソッドチェインの弊害 ―do記法のありがたみ p.265 *)
- /// 関数[2-1] 条件分岐が煩雑な処理の例
- let simulateJavaOptional_2(personName: string): unit =
- printfn "\n 関数[2-1] simulateJavaOptional_2 を実行..."
- let persons = new Persons()
- let person: Optional<Person> = persons.find(personName)
- if person.isPresent() then // Aliceがいて
- let pocket: Optional<Pocket> = person.get().getPocket() // personからpocketを取り出す
- if pocket.isPresent() then // ポケットがあって
- let ticket: Optional<Ticket> = pocket.get().getTicket()
- if ticket.isPresent() then // 引換券を持っている
- let wallet: Optional<Wallet> = person.get().getWallet() // personからwalletを取り出す
- if wallet.isPresent() then // 財布を持っていて
- let money: Optional<Money> = wallet.get().getMoney(10000)
- if money.isPresent() then // 10000円入っている
- pay2(ticket.get(), money.get())
- /// 関数[2-2] Javaのメソッドチェーンを模したコード(その2)
- let simulateJavaMethodChain_2(personName: string): unit =
- printfn "\n 関数[2-2] simulateJavaMethodChain_2 を実行..."
- let persons = new Persons()
- persons.find(personName)
- .ifPresent(fun person ->
- person.getPocket()
- .flatMap(fun pocket ->
- pocket.getTicket())
- .ifPresent(fun ticket ->
- person.getWallet()
- .flatMap(fun wallet -> wallet.getMoney(10000))
- .ifPresent(fun money -> pay2(ticket, money))))
- /// 関数[2-3] F# のパイプライン演算を利用したコード(その2)
- let usePipeline_2(personName: string): unit =
- printfn "\n 関数[2-3] usePipeline_2 を実行..."
- let persons = new Persons()
- persons.find(personName)
- |> Option.iter(fun person ->
- person.getPocket()
- |> Option.bind(fun pocket -> pocket.getTicket())
- |> Option.iter(fun ticket ->
- person.getWallet()
- |> Option.bind(fun wallet -> wallet.getMoney(10000))
- |> Option.iter(fun money -> pay2(ticket, money))))
- //--------------------------------------------------------------------
- /// Option型を扱うコンピューテーション式
- type OptionalBuilder() =
- member __.Bind(x: 'T option, rest: 'T -> 'U option): 'U option =
- Option.bind (fun x -> rest x) x
- member __.Return(v: 'T): 'T option =
- Some v
- member __.Zero(): unit option = // returnがない場合の対策
- Some ()
- /// 関数[2-4] コンピューテーション式で可読性を上げた処理
- let useComputationExpression(personName: string): unit =
- printfn "\n 関数[2-4] useComputationExpression を実行..."
- let optional = new OptionalBuilder()
- optional {
- let persons = new Persons()
- let! person = persons.find personName // Aliceがいて
- let! pocket = person.Pocket // ポケットがあって
- let! ticket = pocket.Ticket // 引換券を持っている
- let! wallet = person.getWallet() // 財布を持っていて
- let! money = wallet.getMoney(10000) // 10000円以上入っていれば出す
- pay2(ticket, money)
- }
- |> ignore
- #if !INTERACTIVE
- [<EntryPoint>]
- #endif
- do
- let personName = "Alice"
- printfn "\n---------------------------------------------"
- simulateJavaOptional_1 personName
- simulateJavaMethodChain_1 personName
- usePipeline_1 personName
- printfn "\n---------------------------------------------"
- simulateJavaOptional_2 personName
- simulateJavaMethodChain_2 personName
- usePipeline_2 personName
- useComputationExpression personName
- printfn "\n---------------------------------------------"
- #if !INTERACTIVE
- Console.ReadKey() |> ignore
- #endif
64行目までは以降の関数群を実行する準備で、処理対象となるデータを構成する判別共用体やレコード型、クラスの宣言などを行っています。判別共用体やレコード型にはメソッド定義も付加できるのでクラスの代わりに利用しています。
以下の図は上記の F# コードで定義している Person 型の構造を表しています。
以下は条件分岐が多い処理の一例としての関数定義 simulateJavaOptional_1
(72行目)です。 Persons
オブジェクトから名前(文字列)を使って Person
オブジェクトを取り出し、if 文で様々な条件を検査しすべて合格した場合は関数 pay1
(59行目)を呼び出しています。条件分岐が多く、ネストが深くなってしまったコードです。 Option 型の型拡張(32~36行目)を利用して、 Java の Optional 型のメソッド呼び出しを F# のコードで模しています。
- /// 関数[1-1] JavaのOptional型を模したコード
- let simulateJavaOptional_1(personName: string): unit =
- printfn "\n 関数[1-1] simulateJavaOptional_1 を実行..."
- let persons = new Persons()
- let person: Optional<Person> = persons.find(personName)
- if person.isPresent() then
- let wallet: Optional<Wallet> = person.get().getWallet()
- if wallet.isPresent() then
- let money: Optional<Money> = wallet.get().getMoney(10000)
- if money.isPresent() then
- pay1(money.get())
上記の関数と同じ動作を、 Java の Optional 型のメソッドチェーンを模した簡潔なコードに直したのが以下のコード(84~91行目)です。
- /// 関数[1-2] Javaのメソッドチェーンを模したコード(その1)
- let simulateJavaMethodChain_1(personName: string): unit =
- printfn "\n 関数[1-2] simulateJavaMethodChain_1 を実行..."
- let persons = new Persons()
- persons
- .find(personName)
- .flatMap(fun person -> person.getWallet())
- .flatMap(fun wallet -> wallet.getMoney(10000))
- .ifPresent(fun money -> pay1(money))
F# にはパイプライン演算子があるので、メソッドチェーンを真似しなくとも簡潔なコード表現ができます。
- /// 関数[1-3] F# のパイプライン演算を利用したコード(その1)
- let usePipeline_1(personName: string): unit =
- printfn "\n 関数[1-3] usePipeline_1 を実行..."
- let persons = new Persons()
- persons.find(personName)
- |> Option.bind(fun person -> person.getWallet())
- |> Option.bind(fun wallet -> wallet.getMoney(10000))
- |> Option.iter(fun money -> pay1(money))
さらに面倒な分岐処理を含む関数(106行目)の例を考えてみます。
- /// 関数[2-1] 条件分岐が煩雑な処理の例
- let simulateJavaOptional_2(personName: string): unit =
- printfn "\n 関数[2-1] simulateJavaOptional_2 を実行..."
- let persons = new Persons()
- let person: Optional<Person> = persons.find(personName)
- if person.isPresent() then // Aliceがいて
- let pocket: Optional<Pocket> = person.get().getPocket() // personからpocketを取り出す
- if pocket.isPresent() then // ポケットがあって
- let ticket: Optional<Ticket> = pocket.get().getTicket()
- if ticket.isPresent() then // 引換券を持っている
- let wallet: Optional<Wallet> = person.get().getWallet() // personからwalletを取り出す
- if wallet.isPresent() then // 財布を持っていて
- let money: Optional<Money> = wallet.get().getMoney(10000)
- if money.isPresent() then // 10000円入っている
- pay2(ticket.get(), money.get())
これをメソッドチェーンを使って書き直すと以下のようになります。今度は読みづらいコードになってしまいました。
- /// 関数[2-2] Javaのメソッドチェーンを模したコード(その2)
- let simulateJavaMethodChain_2(personName: string): unit =
- printfn "\n 関数[2-2] simulateJavaMethodChain_2 を実行..."
- let persons = new Persons()
- persons.find(personName)
- .ifPresent(fun person ->
- person.getPocket()
- .flatMap(fun pocket ->
- pocket.getTicket())
- .ifPresent(fun ticket ->
- person.getWallet()
- .flatMap(fun wallet -> wallet.getMoney(10000))
- .ifPresent(fun money -> pay2(ticket, money))))
パイプライン演算を用いてもこの場合はやはり読みやすくはなりません。
- /// 関数[2-3] F# のパイプライン演算を利用したコード(その2)
- let usePipeline_2(personName: string): unit =
- printfn "\n 関数[2-3] usePipeline_2 を実行..."
- let persons = new Persons()
- persons.find(personName)
- |> Option.iter(fun person ->
- person.getPocket()
- |> Option.bind(fun pocket -> pocket.getTicket())
- |> Option.iter(fun ticket ->
- person.getWallet()
- |> Option.bind(fun wallet -> wallet.getMoney(10000))
- |> Option.iter(fun money -> pay2(ticket, money))))
コンピューテーション式の利用
F# にはコンピューテーション式(Computation Expressions)という文法があります。これを利用して、式を評価する際に背後で動作する副作用を独自に定義できます。F# のコード内でモナドの動作を実現するために利用されることも多いと思います。
153行目からがコンピューテーション式の定義(ビルダークラスとも呼ばれる)です。文法上は F# のクラス定義と変わりませんが、Bind
や Return
などの規定の名前とシグネチャに従ったメソッド群をクラス内で定義しておく必要があります。
- /// Option型を扱うコンピューテーション式
- type OptionalBuilder() =
- member __.Bind(x: 'T option, rest: 'T -> 'U option): 'U option =
- Option.bind (fun x -> rest x) x
- member __.Return(v: 'T): 'T option =
- Some v
- member __.Zero(): unit option = // returnがない場合の対策
- Some ()
162行目から始まる関数 useComputationExpression
において、165~173行目のコードブロック optional {...} の内部が、前出の関数 simulateJavaOptional_2
(106行目)と同じ動作をするコードになります。
ここでは F# の Option 型とコンピューテーション式を活かして、 Haskell における Maybe モナドと do 構文の組み合わせに近いコード表現を試みており、関数 simulateJavaOptional_2
(106行目)と同じ機能でありながら、F# のコードをより簡潔に書くことができます。
- /// 関数[2-4] コンピューテーション式で可読性を上げた処理
- let useComputationExpression(personName: string): unit =
- printfn "\n 関数[2-4] useComputationExpression を実行..."
- let optional = new OptionalBuilder()
- optional {
- let persons = new Persons()
- let! person = persons.find personName // Aliceがいて
- let! pocket = person.Pocket // ポケットがあって
- let! ticket = pocket.Ticket // 引換券を持っている
- let! wallet = person.getWallet() // 財布を持っていて
- let! money = wallet.getMoney(10000) // 10000円以上入っていれば出す
- pay2(ticket, money)
- }
- |> ignore
実行結果
参考資料:
*1:ただし、同書に掲載されているコード片は不完全なものなので、本記事においては、クラス定義など足りない部分については独自解釈でコードを大幅に追加しています。
開発環境メモ - VS 2019 Preview 3.0 のタイトルバーを復活させる
Visual Studio 2019 Preview 3.0 ではタイトルバーの有無を選択するメニュー項目が出来ました。
メニューバーから「 ツール → オプション → 環境 → プレビュー機能」で設定項目にたどり着けます。
タイトルバーを復活させるには、項目「コンパクトメニューと検索バーを使用する」のチェックボックスをOFFに設定してから Visual Studio を再起動させる必要があります。
コンパクトメニュー ON設定時の外観
コンパクトメニュー OFF設定時の外観