Getting a Good Grasp of F# (仮)

関数型言語F#をもっと楽しみたい

煩雑な条件分岐を F# のコンピューテーション式で簡潔に表現する

条件分岐の多い処理を、F# の Option 型コンピューテーション式の文法を活用してコードの可読性を向上させる例を示します。

関数プログラミング実践入門 ──簡潔で、正しいコードを書くために」《第5章 モナド / 5.4 他の言語におけるモナド》に載っている JavaHaskell のコード片を参考に、 F# のコードで同様の動作をするように独自に解釈して実装を試みています*1

 F#で実装したコード

  1. open System
  2.  
  3. type Ticket = FilmTicket
  4. type Money = int
  5. type Wallet = { mutable Amount: Money }
  6.     with
  7.     member this.getMoney (cost: Money) =
  8.         if this.Amount >= cost then
  9.             this.Amount <- this.Amount - cost
  10.             printfn "    財布から%A円出す 残金=%A円" cost this.Amount
  11.             Some cost
  12.         else
  13.             printfn "    お金が足りない"
  14.             None
  15.  
  16. type Pocket = { Wallet: Wallet option; Ticket: Ticket option }
  17.     with
  18.     member this.getWallet(): Wallet option =
  19.         this.Wallet
  20.     member this.getTicket(): Ticket option =
  21.         this.Ticket
  22.  
  23. type Person = { Name: string; Pocket: Pocket option }
  24.     with    
  25.     member this.getWallet(): Wallet option =
  26.         Option.bind (fun x -> x.Wallet) this.Pocket
  27.     member this.getPocket(): Pocket option =
  28.         this.Pocket
  29.  
  30. /// Option型の型拡張:JavaのOptional型と同名・同機能のメソッドを追加
  31. /// オリジナルのJavaコードに外観を似せるための措置
  32. type Option<'T> with
  33.     member this.get() =         this |> Option.get
  34.     member this.isPresent() =   this |> Option.isSome
  35.     member this.flatMap(f) =    this |> Option.bind f
  36.     member this.ifPresent(f) =  this |> Option.iter f
  37.  
  38. type Optional<'T> = Option<'T> // 型のエイリアス
  39.  
  40. type Persons() =
  41.     let personDB = seq [
  42.         {Name = "Alice";
  43.             Pocket = Some {Wallet = {Amount = 10000} |> Some; Ticket = FilmTicket |> Some }}
  44.         {Name = "Becky";
  45.             Pocket = Some {Wallet = {Amount = 20000} |> Some; Ticket = None(* 引換券なし *)}}
  46.         {Name = "Cathy";
  47.             Pocket = Some {Wallet = {Amount = 1000} |> Some; Ticket = FilmTicket |> Some}}
  48.     ]
  49.     let findBase (db: Person seq) (name: string): Person option =
  50.         Seq.tryFind (fun x -> x.Name = name) db
  51.         |> fun person ->
  52.             if person.IsNone then
  53.                 printfn "該当する名前の人物が存在しません: %A" name
  54.             person
  55.  
  56.     member __.find = findBase personDB
  57.  
  58. /// お金を払う(その1)
  59. let pay1 (money: Money): unit =
  60.     printfn "    pay1 実行  Money= %A"  money
  61.  
  62. /// お金を払う(その2)
  63. let pay2 (ticket: Ticket, money: Money): unit =
  64.     printfn "    pay2 実行  Ticket= %A, Money= %A" ticket money
  65.  
  66. //--------------------------------------------------------------------
  67.  
  68. (*  参考資料: 関数プログラミング実践入門 [技術評論社]
  69.        第5章 モナド / 5.4 他の言語におけるモナド / Optionalクラス p.264 *)
  70.  
  71. /// 関数[1-1] JavaのOptional型を模したコード
  72. let simulateJavaOptional_1(personName: string): unit =
  73.     printfn "\n 関数[1-1] simulateJavaOptional_1 を実行..."
  74.     let persons = new Persons()
  75.     let person: Optional<Person> = persons.find(personName)
  76.     if person.isPresent() then
  77.         let wallet: Optional<Wallet> =  person.get().getWallet()
  78.         if wallet.isPresent() then
  79.             let money: Optional<Money> = wallet.get().getMoney(10000)
  80.             if money.isPresent() then
  81.                 pay1(money.get())
  82.  
  83. /// 関数[1-2] Javaのメソッドチェーンを模したコード(その1)
  84. let simulateJavaMethodChain_1(personName: string): unit =
  85.     printfn "\n 関数[1-2] simulateJavaMethodChain_1 を実行..."
  86.     let persons = new Persons()
  87.     persons
  88.         .find(personName)
  89.         .flatMap(fun person -> person.getWallet())
  90.         .flatMap(fun wallet -> wallet.getMoney(10000))
  91.         .ifPresent(fun money -> pay1(money))
  92.  
  93. /// 関数[1-3] F# のパイプライン演算を利用したコード(その1)  
  94. let usePipeline_1(personName: string): unit =
  95.     printfn "\n 関数[1-3] usePipeline_1 を実行..."
  96.     let persons = new Persons()
  97.     persons.find(personName)
  98.     |> Option.bind(fun person -> person.getWallet())
  99.     |> Option.bind(fun wallet -> wallet.getMoney(10000))
  100.     |> Option.iter(fun money -> pay1(money))
  101.     
  102. (*  参考資料: 関数プログラミング実践入門 [技術評論社]
  103.         第5章 モナド / 5.4 他の言語におけるモナド / メソッドチェインの弊害 ―do記法のありがたみ  p.265 *)
  104.  
  105. /// 関数[2-1] 条件分岐が煩雑な処理の例
  106. let simulateJavaOptional_2(personName: string): unit =
  107.     printfn "\n 関数[2-1] simulateJavaOptional_2 を実行..."
  108.     let persons = new Persons()
  109.     let person: Optional<Person> = persons.find(personName)
  110.     if person.isPresent() then // Aliceがいて
  111.         let pocket: Optional<Pocket> = person.get().getPocket() // personからpocketを取り出す
  112.  
  113.         if pocket.isPresent() then // ポケットがあって
  114.             let ticket: Optional<Ticket> = pocket.get().getTicket()
  115.             if ticket.isPresent() then // 引換券を持っている
  116.                 let wallet: Optional<Wallet> = person.get().getWallet() // personからwalletを取り出す
  117.  
  118.                 if wallet.isPresent() then // 財布を持っていて
  119.                     let money: Optional<Money> = wallet.get().getMoney(10000)                       
  120.                     if money.isPresent() then // 10000円入っている
  121.                         pay2(ticket.get(), money.get())
  122.  
  123. /// 関数[2-2] Javaのメソッドチェーンを模したコード(その2)
  124. let simulateJavaMethodChain_2(personName: string): unit =
  125.     printfn "\n 関数[2-2] simulateJavaMethodChain_2 を実行..."
  126.     let persons = new Persons()   
  127.     persons.find(personName)
  128.         .ifPresent(fun person ->
  129.             person.getPocket()
  130.                 .flatMap(fun pocket ->
  131.                     pocket.getTicket())
  132.                         .ifPresent(fun ticket ->
  133.                             person.getWallet()
  134.                                 .flatMap(fun wallet -> wallet.getMoney(10000))
  135.                                 .ifPresent(fun money -> pay2(ticket, money))))
  136.  
  137. /// 関数[2-3] F# のパイプライン演算を利用したコード(その2)  
  138. let usePipeline_2(personName: string): unit =
  139.     printfn "\n 関数[2-3] usePipeline_2 を実行..."
  140.     let persons = new Persons()   
  141.     persons.find(personName)
  142.     |> Option.iter(fun person ->
  143.                     person.getPocket()
  144.                     |> Option.bind(fun pocket -> pocket.getTicket())
  145.                     |> Option.iter(fun ticket ->
  146.                                         person.getWallet()
  147.                                         |> Option.bind(fun wallet -> wallet.getMoney(10000))
  148.                                         |> Option.iter(fun money -> pay2(ticket, money))))
  149.     
  150. //--------------------------------------------------------------------
  151.  
  152. /// Option型を扱うコンピューテーション式
  153. type OptionalBuilder() =
  154.     member __.Bind(x: 'T option, rest: 'T -> 'U option): 'U option =   
  155.         Option.bind (fun x -> rest x) x
  156.     member __.Return(v: 'T): 'T option =
  157.         Some v
  158.     member __.Zero(): unit option = // returnがない場合の対策
  159.         Some ()
  160.  
  161. /// 関数[2-4] コンピューテーション式で可読性を上げた処理
  162. let useComputationExpression(personName: string): unit =
  163.     printfn "\n 関数[2-4] useComputationExpression を実行..."
  164.     let optional = new OptionalBuilder()
  165.     optional {
  166.         let persons = new Persons()
  167.         let! person = persons.find personName    // Aliceがいて
  168.         let! pocket = person.Pocket         // ポケットがあって
  169.         let! ticket = pocket.Ticket         // 引換券を持っている
  170.         let! wallet = person.getWallet()    // 財布を持っていて
  171.         let! money = wallet.getMoney(10000) // 10000円以上入っていれば出す
  172.         pay2(ticket, money)
  173.     }
  174.     |> ignore
  175.  
  176. #if !INTERACTIVE
  177. [<EntryPoint>]
  178. #endif
  179. do   
  180.     let personName = "Alice"
  181.     printfn "\n---------------------------------------------"
  182.     simulateJavaOptional_1 personName
  183.     simulateJavaMethodChain_1 personName
  184.     usePipeline_1 personName
  185.     printfn "\n---------------------------------------------"
  186.     simulateJavaOptional_2 personName
  187.     simulateJavaMethodChain_2 personName
  188.     usePipeline_2 personName
  189.     useComputationExpression personName
  190.     printfn "\n---------------------------------------------"
  191.  
  192. #if !INTERACTIVE
  193.     Console.ReadKey() |> ignore
  194. #endif

 

64行目までは以降の関数群を実行する準備で、処理対象となるデータを構成する判別共用体レコード型、クラスの宣言などを行っています。判別共用体レコード型にはメソッド定義も付加できるのでクラスの代わりに利用しています。

以下の図は上記の F# コードで定義している Person 型の構造を表しています。

Person型(レコード型)
フィールド Name: string型
フィールド Pocket: Option<Pocket>型(Pocket型はレコード型)
フィールド Ticket: Option<Ticket>型(Ticket型は判別共用体)
フィールド Wallet: Option<Wallet>型(Wallet型はレコード型)
フィールド Amount: Money型(int型のエイリアス

 

以下は条件分岐が多い処理の一例としての関数定義 simulateJavaOptional_172行目)です。 Persons オブジェクトから名前(文字列)を使って Person オブジェクトを取り出し、if 文で様々な条件を検査しすべて合格した場合は関数 pay159行目)を呼び出しています。条件分岐が多く、ネストが深くなってしまったコードです。 Option 型型拡張32~36行目)を利用して、 Java の Optional 型のメソッド呼び出しを F# のコードで模しています。

  1. /// 関数[1-1] JavaのOptional型を模したコード
  2. let simulateJavaOptional_1(personName: string): unit =
  3.     printfn "\n 関数[1-1] simulateJavaOptional_1 を実行..."
  4.     let persons = new Persons()
  5.     let person: Optional<Person> = persons.find(personName)
  6.     if person.isPresent() then
  7.         let wallet: Optional<Wallet> =  person.get().getWallet()
  8.         if wallet.isPresent() then
  9.             let money: Optional<Money> = wallet.get().getMoney(10000)
  10.             if money.isPresent() then
  11.                 pay1(money.get())

 

上記の関数と同じ動作を、  Java の Optional 型のメソッドチェーンを模した簡潔なコードに直したのが以下のコード(84~91行目)です。

  1. /// 関数[1-2] Javaのメソッドチェーンを模したコード(その1)
  2. let simulateJavaMethodChain_1(personName: string): unit =
  3.     printfn "\n 関数[1-2] simulateJavaMethodChain_1 を実行..."
  4.     let persons = new Persons()
  5.     persons
  6.         .find(personName)
  7.         .flatMap(fun person -> person.getWallet())
  8.         .flatMap(fun wallet -> wallet.getMoney(10000))
  9.         .ifPresent(fun money -> pay1(money))

 

F# にはパイプライン演算子があるので、メソッドチェーンを真似しなくとも簡潔なコード表現ができます。

  1. /// 関数[1-3] F# のパイプライン演算を利用したコード(その1)  
  2. let usePipeline_1(personName: string): unit =
  3.     printfn "\n 関数[1-3] usePipeline_1 を実行..."
  4.     let persons = new Persons()
  5.     persons.find(personName)
  6.     |> Option.bind(fun person -> person.getWallet())
  7.     |> Option.bind(fun wallet -> wallet.getMoney(10000))
  8.     |> Option.iter(fun money -> pay1(money))

 

さらに面倒な分岐処理を含む関数(106行目)の例を考えてみます。

  1. /// 関数[2-1] 条件分岐が煩雑な処理の例
  2. let simulateJavaOptional_2(personName: string): unit =
  3.     printfn "\n 関数[2-1] simulateJavaOptional_2 を実行..."
  4.     let persons = new Persons()
  5.     let person: Optional<Person> = persons.find(personName)
  6.     if person.isPresent() then // Aliceがいて
  7.         let pocket: Optional<Pocket> = person.get().getPocket() // personからpocketを取り出す
  8.  
  9.         if pocket.isPresent() then // ポケットがあって
  10.             let ticket: Optional<Ticket> = pocket.get().getTicket()
  11.             if ticket.isPresent() then // 引換券を持っている
  12.                 let wallet: Optional<Wallet> = person.get().getWallet() // personからwalletを取り出す
  13.  
  14.                 if wallet.isPresent() then // 財布を持っていて
  15.                     let money: Optional<Money> = wallet.get().getMoney(10000)                       
  16.                     if money.isPresent() then // 10000円入っている
  17.                         pay2(ticket.get(), money.get())

これをメソッドチェーンを使って書き直すと以下のようになります。今度は読みづらいコードになってしまいました。

  1. /// 関数[2-2] Javaのメソッドチェーンを模したコード(その2)
  2. let simulateJavaMethodChain_2(personName: string): unit =
  3.     printfn "\n 関数[2-2] simulateJavaMethodChain_2 を実行..."
  4.     let persons = new Persons()   
  5.     persons.find(personName)
  6.         .ifPresent(fun person ->
  7.             person.getPocket()
  8.                 .flatMap(fun pocket ->
  9.                     pocket.getTicket())
  10.                         .ifPresent(fun ticket ->
  11.                             person.getWallet()
  12.                                 .flatMap(fun wallet -> wallet.getMoney(10000))
  13.                                 .ifPresent(fun money -> pay2(ticket, money))))

 パイプライン演算を用いてもこの場合はやはり読みやすくはなりません。

  1. /// 関数[2-3] F# のパイプライン演算を利用したコード(その2)  
  2. let usePipeline_2(personName: string): unit =
  3.     printfn "\n 関数[2-3] usePipeline_2 を実行..."
  4.     let persons = new Persons()   
  5.     persons.find(personName)
  6.     |> Option.iter(fun person ->
  7.                     person.getPocket()
  8.                     |> Option.bind(fun pocket -> pocket.getTicket())
  9.                     |> Option.iter(fun ticket ->
  10.                                         person.getWallet()
  11.                                         |> Option.bind(fun wallet -> wallet.getMoney(10000))
  12.                                         |> Option.iter(fun money -> pay2(ticket, money))))

 

コンピューテーション式の利用

F# にはコンピューテーション式(Computation Expressions)という文法があります。これを利用して、式を評価する際に背後で動作する副作用を独自に定義できます。F# のコード内でモナドの動作を実現するために利用されることも多いと思います。

153行目からがコンピューテーション式の定義(ビルダークラスとも呼ばれる)です。文法上は F# のクラス定義と変わりませんが、BindReturn などの規定の名前とシグネチャに従ったメソッド群をクラス内で定義しておく必要があります。

  1. /// Option型を扱うコンピューテーション式
  2. type OptionalBuilder() =
  3.     member __.Bind(x: 'T option, rest: 'T -> 'U option): 'U option =   
  4.         Option.bind (fun x -> rest x) x
  5.     member __.Return(v: 'T): 'T option =
  6.         Some v
  7.     member __.Zero(): unit option = // returnがない場合の対策
  8.         Some ()

 

162行目から始まる関数 useComputationExpression において、165~173行目のコードブロック optional {...} の内部が、前出の関数 simulateJavaOptional_2 (106行目)と同じ動作をするコードになります。

ここでは F# の Option 型コンピューテーション式を活かして、 Haskell における Maybe モナドと do 構文の組み合わせに近いコード表現を試みており、関数 simulateJavaOptional_2 (106行目)と同じ機能でありながら、F# のコードをより簡潔に書くことができます。

  1. /// 関数[2-4] コンピューテーション式で可読性を上げた処理
  2. let useComputationExpression(personName: string): unit =
  3.     printfn "\n 関数[2-4] useComputationExpression を実行..."
  4.     let optional = new OptionalBuilder()
  5.     optional {
  6.         let persons = new Persons()
  7.         let! person = persons.find personName    // Aliceがいて
  8.         let! pocket = person.Pocket         // ポケットがあって
  9.         let! ticket = pocket.Ticket         // 引換券を持っている
  10.         let! wallet = person.getWallet()    // 財布を持っていて
  11.         let! money = wallet.getMoney(10000) // 10000円以上入っていれば出す
  12.         pay2(ticket, money)
  13.     }
  14.     |> ignore

 

実行結果

 

参考資料:

関数プログラミング実践入門 ──簡潔で、正しいコードを書くために技術評論社

 第5章 モナド / 5.4 他の言語におけるモナド

 

*1:ただし、同書に掲載されているコード片は不完全なものなので、本記事においては、クラス定義など足りない部分については独自解釈でコードを大幅に追加しています。