F# の良さを示す短いサンプルコードを書きました。トランプゲームを題材とする「判別共用体」機能の概略です。
(1/4) 網羅的なパターンマッチ
カードのスート(柄)を表す Suit 型を「判別共用体」として定義します。ここでは C# などの enum と同様です。
type Suit =
| Spade
| Clover
| Heart
| Diamond
使用例として、スートの名前を取得する関数を定義します。
let suitToName suit =
match suit with
| Spade -> "スペード"
| Clover -> "クローバー"
| Heart -> "ハート"
| Diamond -> "ダイヤ"
match...with
は switch
のような場合分け構文です。場合分けに漏れがあるときはコンパイル時に警告する という機能があります。
まずは正常な動作例を見ます:
printfn "%s" (suitToName Spade) //=> スペード
そして、コンパイル時の警告も見てみましょう。Diamond が渡されたときのケースをコメントアウトして、ビルドしてみます。
match suit with
| Spade -> "スペード"
| Clover -> "クローバー"
| Heart -> "ハート"
// | Diamond -> "ダイヤ"
エディタやビルド出力にこんな警告が出ます:
warning FS0025:
この式のパターン マッチが不完全です
たとえば、値 'Diamond' はパターンに含まれないケースを示す可能性があります。
(2/4) フィールドを持つ判別共用体
次に、トランプのカードを表す Card 型を「判別共用体」として定義します。判別共用体の各ケースは、普通の enum と違って、フィールドを持つことができます。
type Card =
| NormalCard of suit:Suit * rank:int
| Joker
of
の後ろにある suit:Suit
などがフィールドの定義です。(*
で区切る。)
この定義には次の2つの事実がほぼそのまま書かれていることが見て取れると思います。
- 普通のカード(NormalCard)はスート(柄)とランク(数字)を持つ
- ジョーカー(Joker)はフィールドを持たない
フィールドを持つケースはクラスのように、コンストラクタでインスタンス化できます。
let heart3 = NormalCard (Heart, 3)
使用例として、カードの名前を取得する関数を定義します。場合分けしてケースごとに実装を書くのは上と同じです。
let cardToName card =
match card with
| Joker ->
"ジョーカー"
| NormalCard (suit, rank) ->
let suitName = suitToName suit
let rankName = string rank
suitName + "の" + rankName
実行例:
printfn "Joker: %s" (cardToName Joker) //=> ジョーカー
printfn "Heart3: %s" (cardToName heart3) //=> ハートの3
「判別共用体」の使い道はいくらでも見つけられます。
type HttpResponse =
| OkWithText of text:string // 200
| OkWithJson of json:obj // 200
| Redirect of uri:string * temp:bool // 301 or 302
| InternalError of ex:exn // 500
type BinaryTree<'T> =
| Node of left:BinaryTree<'T> * right:BinaryTree<'T>
| Leaf of value:'T
type Contact =
| ContactWithMail
of address:string
| ContactWith郵送
of 郵便番号:string * 住所:string * 名前:string
(3/4) 型推論
ところで、上で定義した関数は 引数や結果の型を全く書いていない のにコンパイルできました。しかし、動的検査というわけではありません。型エラーがあれば、コンパイルエラーとして報告されます。
信じられない人は、これをコンパイルしてみてください。
printfn "%s" (cardToName "ジョーカー")
// ^^^^^^^^^^ 本来は Card 型の定数 Joker が正解
※public な関数の型は書きましょう。
(4/4) まとめ
本稿が主張する F# の良さは 簡潔さ です! 判別共用体を抽象クラスと継承で定義したら、なかなかのコード量になるのではないでしょうか。
他にもたくさん良い点がありますが、そろそろ時間なのでここまで。わずかでも興味を持っていただけたら幸いです。
おまけ: 練習問題
ブラウザ上で F# を試すことも可能 です: try.fsharp.org で本稿のコードを試す
かんたんな練習問題もつけておいたので、興味がある人はやってみてください。
// 「ハートのエース」と表示されるようにしてみよう
printfn "HeartA: %s" (cardToName (NormalCard (Heart, 1))) //=> ハートの1
// 「ハートのキング」と表示されるようにしてみよう
printfn "HeartK: %s" (cardToName (NormalCard (Heart, 13))) //=> ハートの13
// ヒント:
let rankToName rank =
match rank with
| 1 -> "エース"
| _ -> "エースではない"
もっと F# を知りたい人へ
-
F# を知ってほしい
- 詳細度の高い F# 概要
-
DDDを.NETでやるならドメイン層はF#が良いんじゃないか、という話
- F# とドメイン層の型定義の相性について
- F#リファレンス
-
Ionide - Crossplatform F# Editor Tools
- ionide = F# の開発ツールを作ってる人々
- GitHub にある本稿のサンプルコード 2020-02-09-fsharp-goodness-playing-card/Program.fs