5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

5分で眺める F# 入門(1) トランプカードと判別共用体

Last updated at Posted at 2020-02-08

F# の良さを示す短いサンプルコードを書きました。トランプゲームを題材とする「判別共用体」機能の概略です。

(1/4) 網羅的なパターンマッチ

カードのスート(柄)を表す Suit 型を「判別共用体」として定義します。ここでは C# などの enum と同様です。

type Suit =
    | Spade
    | Clover
    | Heart
    | Diamond

使用例として、スートの名前を取得する関数を定義します。

let suitToName suit =
    match suit with
    | Spade     -> "スペード"
    | Clover    -> "クローバー"
    | Heart     -> "ハート"
    | Diamond   -> "ダイヤ"

match...withswitch のような場合分け構文です。場合分けに漏れがあるときはコンパイル時に警告する という機能があります。

まずは正常な動作例を見ます:

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 -> "エース"
    | _ -> "エースではない"

(try.fsharp.org で解答例を見る)

もっと F# を知りたい人へ

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?