煩雑な条件分岐を 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:ただし、同書に掲載されているコード片は不完全なものなので、本記事においては、クラス定義など足りない部分については独自解釈でコードを大幅に追加しています。