8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Do'erAdvent Calendar 2022

Day 19

ミニゲームを作って学ぶ「F#」

Last updated at Posted at 2022-12-18

この記事は、Do'er Advent Calenderの19日目の記事です。
記事の最後にミニゲームのコードのリンクを貼っているので、ぜひお手元の環境でお試しください!

はじめに

F#が大好きな情報系学科3回生で、今年4月にDo'erに所属しました。このような機会を頂いたので、推し言語「F#」について紹介させていただきます!

もし何かご質問、ご指摘等ございましたら、Twitterの方に連絡していただけると嬉しいです。

F#ってどんな言語?

F#とは関数型プログラミングが可能な、マルチパラダイム言語です。

F#では、強力な型推論と、厳密なコンパイラチェック、「1入力1出力の関数に不変な値を与え、一意な結果を得る」という関数型スタイルが組み合わさることにより、人間工学的な安全性が実現されています。

簡単に魅力の一部を挙げると、

  1. 可読性の高さ
    • 小さな関数を組み合わせることで、効率よく大規模なプログラムを構築できます。処理の流れが明確になり、プログラムが理解しやすくなります。
    • 軽量構文が備わり、見通しの良いコードが書けます。
  2. 制約条件を型で表現可能
    • Nullフリー
      • Option型を用いると、値がない場合の処理を強制できます。
      • 「その変数はNullかもしれないので、使用する前にNullかどうかチェックしてください」というドキュメントは不要です。
  3. ハイブリッド言語
    • オブジェクト指向も可能です。
      • C#製のライブラリとの互換性が確保されています。
      • データによっては、オブジェクトとして表現した方が適切な場合もあるかもしれません。
    • 寛容で、小回りが効きます。
      • 可変な変数も定義できます。また、副作用を発生させる場所に制約がありません。

他にもまだまだありますが、私が熱い想いで色々語っても、特に興味がない人にとっては、大学のつまらない講義のようなものでしょう。

「百聞は一見に如かず」です。まずF#とはどんなプログラミング言語なのかを知ってもらうために、150行程度のミニゲームを作ってみました。細かい文法の解説は他のサイトに任せるとして、エッセンスを凝縮したので、雰囲気だけでも見ていってください。

こちらからブラウザ上でF#が試せるので、一緒に手を動かしながらF#を体験しましょう!

どんなミニゲーム?

「Papers, Please」という、「アルストツカに栄光あれ!」でおなじみのゲームの実況動画が面白かったので、入国審査のミニゲームを作りたいと思います。

ゲームの流れ

  1. プレイヤー(入国審査官)は書類を受け取る
  2. ハンコを押す

をミニゲームの流れとします。いたってシンプルですね。
次の手順で、ミニゲームを作っていきましょう。

  1. 型定義を通して、問題をモデリングして大まかな設計を固めます。
  2. 関数を実装して、詳細を詰めます。

1。型定義

まず、入国審査ゲームに必要不可欠な要素といえば国です。

そこで、「京都国」「滋賀国」「大阪国」を定義します。

open System
type Country = | Kyoto | Siga | Osaka

入管でチェックする書類は「パスポート」と「入国許可証」です。

type Passport = 
    {  
        Name : string //氏名
        PassportNo : int // 旅券番号
        ReigisteredDomicile : Country // 国
        Expire : DateTime // 有効期限
    }  
type EntryPermit =
    { 
        Name : string // 氏名
        PassportNo : int // 旅券番号
        EffectiveDate : DateTime // 当日券のみ有効にします
    }  

チェックする書類はまとめて「Papers」型として扱いましょう.

Option型で定義することによって、書類が不足する可能性を自然に表せますね。

type Papers = {
    Passport : Passport option // Option<Passport>型
    EntryPermit : EntryPermit option // Option<EntryPermit>型
}

いよいよ、このゲームをモデリングしましょう!

ここで一旦、入国審査に必要な情報を整理します。

  • 日付(書類の期限切れ、有効日であるか確認する)
  • 書類(入管が審査する)

そして、プレイヤーはこれらの情報を基に、スタンプ(承認または拒否)を押します。

入国拒否スタンプを押す場合は、その理由も付け加えます。(その方が親切ですよね)

type Reason =
    | ForgedDoc // 偽造書類
    | PassportExpired // パスポートの有効期限切れ
    | InvalidEntryPermit // 無効な入国許可証
    | LackOfDoc // 書類の不足

type Stamp = 
    | APPROUED // 承認
    | DENIED of reason : Reason  // 拒否

type ImmigrationCheck = // 入国審査の流れを表す関数の型
    DateTime 
        -> Papers 
        -> Stamp 

最後に、ゲームの流れをモデリングすれば、型定義は完成です!

type Game =
    DateTime  
        -> Papers[]
        -> unit

2。実装

入管ゲームは、プレイヤー入力の正誤判定が必須です。そのため、

  • プレイヤーによる入国審査の関数(player)
  • 正しい審査結果を返す関数(cpu)

の2つのImmigrationCheck関数が、Game関数を定義するには必要ですね。これらの関数は後で定義するとして、ゲーム全体の流れを定義します。

let game (player : ImmigrationCheck) (cpu: ImmigrationCheck): Game = 
    fun today papers  ->  
        let playerAns = // プレイヤーの回答
            papers
            |> Array.map(fun paper -> player today paper )  
        let ans = // 正しい答え
            papers
            |> Array.map (cpu today) 
        printfn "#############################################"
        Array.zip playerAns ans
        |> Array.iteri(fun i (player, answer) ->
            printf "[%2d] " i
            // 結果を表示
            match player, answer with 
            | APPROUED, APPROUED  -> printfn "OK" 
            | DENIED reason1, DENIED reason2 when (reason1 = reason2) -> 
                printfn "OK(%A)"  answer
            | _, _ -> printfn "× %A -> ○ %A" player answer
        )

入管ルール

  • 偽造書類でないこと(拒否理由 → ForgedDoc
    • パスポートに記載されている氏名と、入国許可証に記載された氏名が一致する
    • 入国許可証に記載された旅券番号が正しいこと
  • パスポートが有効であること(拒否理由 → PassportExpired
  • 入国許可証の有効であること(拒否理由 → InvalidEntryPermit
  • 書類が不足していないこと(拒否理由 → LackOfDoc

このルールにしたがって、正しい審査結果を返す関数を書きます。

let autoChecker: ImmigrationCheck =
    fun today doc -> 
        // Option型でラップした値にアクセスするには、パターンマッチをして
        // 先に値が存在するか確認する必要があります。
        match doc.Passport, doc.EntryPermit with 
        | Some passport, Some entryTicket
             // 両方Someであること加え、when内の条件を満たす場合、マッチします。
            when (
                passport.Name = entryTicket.Name
                && passport.PassportNo = entryTicket.PassportNo
                && passport.Expire >= today
                && entryTicket.EffectiveDate = today
            )
            -> APPROUED
            
        | Some passport, Some entryTicket ->
            DENIED(
                if passport.Name <> entryTicket.Name
                    || passport.PassportNo <> entryTicket.PassportNo then 
                    ForgedDoc // 偽造
                elif passport.Expire < today then 
                    PassportExpired // パスポートの有効期限切れ
                else 
                    InvalidEntryPermit // 入国許可証が無効
            )
        // いずれかの書類がNoneの場合、以下にマッチします
        | None, None // 手ぶら
        | None, _    // パスポートがない
        | _, None -> // 入国許可証がない
            DENIED LackOfDoc // 書類が不足

入管ゲームに必要な最後の関数、プレイヤーによる入国審査の関数を定義します。

  • プレイヤーは、入国を許可する場合は、「y」を入力します。
  • 入国を拒否する場合は、「n」を入力してから、理由に対応する数字(1~4)を入力します。
let playerChecker : ImmigrationCheck =
    fun today doc -> 
        printfn "---------------------------------------------------"
        printfn "[%A]" today
        printfn "%A" doc
        printf "入国許可(y/n) > "
        let stamp = 
            match Console.ReadLine() with 
            | "y" -> APPROUED
            | _ ->   
                printfn "Reason :"
                printfn "①偽造書類 -> 1"
                printfn "②パスポートが期限切れ -> 2"
                printfn "③入国許可証が無効 -> 3"
                printfn "④書類が不足 -> 4"
                printf  "> "
                match Console.ReadLine() with 
                | "1" -> ForgedDoc
                | "2" -> PassportExpired
                | "3" -> InvalidEntryPermit
                | _ ->  LackOfDoc                    
                |> DENIED  
        stamp 

全部繋げたら、入国審査ゲームの関数の完成です!

let papersPlease : Game = game playerChecker autoChecker

後は問題を作成して、実行します。

[<EntryPoint>]
let main _ =
    let today = DateTime(2022,12,25) // メリークリスマス!!
    let papers = [| // 問題の配列、Paper[]型
       {
           Passport = Some {
               Name = "うっかりさん"
               PassportNo = 11
               ReigisteredDomicile = Kyoto
               Expire = today - TimeSpan.FromDays 10
           }

           EntryPermit = Some { 
               Name = "うっかりさん"
               PassportNo = 11
               EffectiveDate = today
           }
       }
    |] 
    papersPlease today papers
    0

ひとまとめにしたソースコードはGistにあげました。こちらからご覧ください

最後に

簡潔で美しく、堅牢なコードが書ける

これが、私がF#を愛する理由の全てです。問題を型定義からトップダウンに解決するの滅茶苦茶楽しいし、自分の書いたコードを読みながら悦に浸る時間はたまりません。加えて、ググったときに質の高い情報にヒットしやすく、独学がしやすいというのも推しポイントです。

ちなみに、普段はこんなコード書いてます。

テトリス:https://github.com/RyushiAok/Tetris
深層強化学習:https://github.com/RyushiAok/CartPole_DRL
オセロ:https://github.com/RyushiAok/othello
ポートフォリオ:https://ryushiaok.dev/

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?