2
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?

More than 5 years have passed since last update.

F#Advent Calendar 2016

Day 24

情報隠蔽とモジュールとシグネチャファイル ~オフラインリアルタイムどう書くE04~

Last updated at Posted at 2016-12-31

この記事は、F# Advent Calendar 2016の24日目に間に合わなかった記事です。(ギリギリ年内になりました。)

情報隠蔽の概要

「インターフェースと実装の分離」や、より広く「情報隠蔽」などと言われるものは、多くのプログラミング言語で実現可能です。たとえばC#やJavaでは、interfaceclassを使い分けたり、クラスの中でもpublicprivateなどといったアクセス修飾子でアクセシビリティを制御したりします。もちろんF#でも同じことができます。

Module1.fs
module Module1

type internal MyInternalType () =
   let x = 5 // クラス内のlet束縛はデフォルトでprivate
   member private this.X() = 10
   member this.Z() = x * 100

F#ではlet束縛でモジュールに値や関数を定義することができますが、こちらにもアクセス修飾子をつけることができます。

Module1.fs
module Module1

let internal myInternalObj = new MyInternalType()
let private add x y = x + y

さて、F#にはレコード型や判別共用体という便利な型もあるのですが、これらの情報隠蔽はどうなるでしょうか。

レコード型や判別共用体は、パターンマッチを便利に使うことができますが、それは内部実装をすべて公開しているからこそです。レコード型については、フィールドは自動的にプロパティとして公開されますし、インスタンスを初期化するときには全フィールドに何らかの値を設定することになります。一部のフィールドを非公開にすることもできませんし、デフォルトのコンストラクタをprivate修飾して独自のコンストラクタのみを使わせることもできません。また、判別共用体については、すべてのコンストラクタ(識別子)が公開されます。一部のコンストラクタを非公開にしたいと思っても、そうする方法はありません。

では、あるモジュールにレコード型や判別共用体を定義してそれをモジュール外に公開したときに、モジュール内ではでパターンマッチを使いつつ、モジュール外ではその型の内部実装にアクセスできずパターンマッチもできないようにすることは可能でしょうか?答えはイエスです。それには、シグネチャファイルを使います。

たとえばモジュールの実装がこうだったとします。

Module1.fs
module Module1

type Type1 = { X: int; Y: int; }
let value1 = { X=1; Y=2; }
type Type2 = A | B
let value2 = A

このファイルと対になるシグネチャファイル(拡張子 .fsi) を用意します。

Module1.fsi
module Module1

type Type1
val value1: Type1
type Type2
val value2: Type2

すると、このモジュールの外からは、シグネチャファイルに書かれた情報しか参照できません。モジュールに型 Type1Type2が定義されていることはわかりますが、これらの実装詳細は隠されるため、レコード型や判別共用体であることはわかりません。したがって、モジュール外ではvalue1からパターンマッチでフィールドの値を取り出したり、value2からパターンマッチで処理を分岐させることもできません。

ただ、このように書くと、Type1Type2が何らかのインターフェースを実装しているかどうかも外部からはわからなくなってしまいます。実際、レコード型や判別共用体は自動的にIEquatable<_>, IStructuralEquatable, IComparable, IStructuralComparableといったインターフェースを実装しますが、それが外部からは見えなくなるため、equality制約やcomparison制約が要求されるジェネリック型にType1Type2をパラメーターとして渡すことができなくなってしまいます。それだと困る場合は、シグネチャファイルを次のように書きます。

Module1.fsi
module Module1

[<Sealed>]
type Type1 =
    interface System.IComparable
val value1: Type1
[<Sealed>]
type Type2 =
    interface System.IComparable
val value2: Type2

このように、interfaceキーワードを使って、実装するインターフェースを表明します。また、その際は型に[<Sealed>][<Class>]などの属性をつける必要もあります。この例の実装型はシールされたレコード型や判別共用体のため、[<Sealed>]属性をつけます。

実践 ~オフラインリアルタイムどう書くE04を題材に~

さて、ここまでは前置きでして、ここからはオフラインリアルタイムどう書くE04というイベントで出題されたコーディング問題をF#で解いてみます。その際に、あえてモジュールで情報隠蔽をしながらやってみます。

出題された問題はこちら、「五角形の世界であなたは過去の自分に出会う」です。

さて、この問題をプログラムに落とし込むには、どうやら「どの五角形の上にいて、どの辺を向いているのか」を表現できなければならないことがわかります。その方法を考えてみましょう。(F#に関係しない話ですが。)

まず、五角形を4つまとめると六角形になり、それが平面を埋め尽くしていることに気づきます。
hex1.png

であれば、「どの五角形の上にいて、どの辺を向いているのか」は、「どの六角形の上にいるか」+「六角形の中にある五角形のどれか」+「どの辺を向いているのか」という3つの表現に分割できます。

「どの六角形の上にいるか」は、直行しないx-y座標軸をとることで、2つの整数の組で表すことができそうです。
hex2.png

「六角形の中にある五角形のどれか」は、4通りが考えられます。ここでは東西南北(North, East, South, West)であらわすことにします。

そして最後に、「どの辺を向いているのか」です。これは五角形ですから5通りが考えられます。それぞれa, b, c, d, eとしましょう。どの辺にどの記号を割り当てるかは自由に決められますが、ここではNorthとSouth、EastとWestが向かい合わせになっている辺をaとして、そこから時計回りにb, c, d, eと割り振ります。

hex3.png

ではF#の型に落としてみましょう。「どの六角形の上にいるか」をHexモジュールのHex型、「六角形の中にある五角形のどれか」をPentagonモジュールのPentagon型、「どの辺を向いているのか」をSideモジュールのSide型に定義します。そしてそれら3つをまとめたものをCoordinatesモジュールのCoordinates型としましょう。

Hex.fs
module Hex

type Hex = {X:int; Y:int}
Pentagon.fs
module Pentagon

type Pentagon = North | East | South | West
Side.fs
module Side

type Side = {I:int}
Coordinates.fs
module Coordinates

type Coordinates = Coordinates of Hex * Pentagon * Side

Side型は5択なので判別共用体で定義してもいいのですが、あえて数値をラップした型として定義してみました。Coordinates型はレコード型でもいいのですが、ちょっとひねって、単一の判別共用体でタプルをラップしてみました。

さて、これをベースに、シグネチャファイルを用意して情報隠蔽しながら、実装を膨らましてみましょう。

まずはHex型。レコード型であることを隠蔽すると、外部からはパターンマッチもコンストラクタも使えませんので、モジュールに即値や生成関数を定義してやります。初期値を起点として、辺をまたぐ形でマス目を移動するわけですから、シグネチャはこんな感じでしょうか。

Hex.fsi
module Hex

type Hex
val origin: Hex
val north:     Hex -> Hex
val northeast: Hex -> Hex
val southeast: Hex -> Hex
val south:     Hex -> Hex
val southwest: Hex -> Hex
val northwest: Hex -> Hex

実装はこうです。

Hex.fs
module Hex

type Hex = {X:int; Y:int}
let origin = {X=0; Y=0}
let north     {X=x; Y=y} = {X=x;   Y=y-1}
let northeast {X=x; Y=y} = {X=x+1; Y=y  }
let southeast {X=x; Y=y} = {X=x+1; Y=y+1}
let south     {X=x; Y=y} = {X=x;   Y=y+1}
let southwest {X=x; Y=y} = {X=x-1; Y=y  }
let northwest {X=x; Y=y} = {X=x-1; Y=y-1}

Pentagon型は、この型単体で完結するロジックがなく(せいぜい、初期値がNorthであることぐらいです)、判別共用体の実装を隠蔽してしまうと逆に外から利用するのが面倒になってしまいそうなので、先ほどの定義のまま、シグネチャファイルも書きません。

そしてSide型ですが、初期値が(Northの)aであることと、時計周り/反時計回りのロジックを持たせることができます。また、次の図をよく見ると、向かい合わせになっている辺に法則があることがわかります。

hex4.png

それは、aとaが向かい合わせになるのは最初の定義の通りですが、それ以外は、bとc、dとeが常に向かい合わせになっています。これもロジックとして抜き出しておきましょう。

Side.fsi
module Side

type Side
val side: int -> Side
val sideA: Side
val clockwise: Side -> Side
val anticlockwise : Side -> Side
val flip : Side -> Side
val (|A|B|C|D|E|) : Side -> Choice<unit,unit,unit,unit,unit>
Side.fs
module Side

type Side = {I:int}
let side i = {I=if i >= 0 then i % 5 else 5 + i % 5}
let sideA = side 0
let clockwise     {I=i} = i + 1 |> side
let anticlockwise {I=i} = i - 1 |> side
let flip          {I=i} =
    match i with
    | 0 -> 0
    | 1 -> 2
    | 2 -> 1
    | 3 -> 4
    | _ -> 3
    |> side
let (|A|B|C|D|E|) {I=i} =
    match i with
    | 0 -> A
    | 1 -> B
    | 2 -> C
    | 3 -> D
    | _ -> E

Sideモジュールの最後に定義した (|A|B|C|D|E|)はアクティブパターンです。判別共用体を実装して公開しない場合でも、アクティブパターンを公開すれば、実装の詳細は隠したままパターンマッチによる処理の分岐を実現できます。というか、それを見せたいがためにわざと判別共用体による実装にしなかったのでした。

ここまで揃ったら最後にCoordinates型です。初期値と、時計回り/反時計回り/反転のコマンドに対応する移動ロジックを定義してみましょう。反転は結構ややこしいですが、丁寧に場合分けすれば大丈夫です。(ここでSideモジュールのアクティブパターンを使います。)

Coordinates.fsi
module Coordinates

type Coordinates
val origin: Coordinates
val clockwise:     Coordinates -> Coordinates
val anticlockwise: Coordinates -> Coordinates
val flip:          Coordinates -> Coordinates
Coordinates.fs
module Coordinates

type Coordinates = Coordinates of Hex.Hex * Pentagon.Pentagon * Side.Side
let origin = Coordinates (Hex.origin, Pentagon.North, Side.sideA)
let clockwise     (Coordinates (h, p, d)) = Coordinates (h, p, Side.clockwise d)
let anticlockwise (Coordinates (h, p, d)) = Coordinates (h, p, Side.anticlockwise d)
let flip          (Coordinates (h, p, d)) =
    match p with
    | Pentagon.North ->
        match d with
        | Side.A -> Coordinates (Hex.north h,     Pentagon.South, Side.flip d)
        | Side.B -> Coordinates (Hex.northeast h, Pentagon.West,  Side.flip d)
        | Side.C -> Coordinates (h,               Pentagon.East,  Side.flip d)
        | Side.D -> Coordinates (h,               Pentagon.West,  Side.flip d)
        | Side.E -> Coordinates (Hex.northwest h, Pentagon.East,  Side.flip d)
    | Pentagon.East  ->
        match d with
        | Side.A -> Coordinates (h,               Pentagon.West,  Side.flip d)
        | Side.B -> Coordinates (h,               Pentagon.North, Side.flip d)
        | Side.C -> Coordinates (Hex.northeast h, Pentagon.South, Side.flip d)
        | Side.D -> Coordinates (Hex.southeast h, Pentagon.North, Side.flip d)
        | Side.E -> Coordinates (h,               Pentagon.South, Side.flip d)
    | Pentagon.South ->
        match d with
        | Side.A -> Coordinates (Hex.south h,     Pentagon.North, Side.flip d)
        | Side.B -> Coordinates (Hex.southwest h, Pentagon.East,  Side.flip d)
        | Side.C -> Coordinates (h,               Pentagon.West,  Side.flip d)
        | Side.D -> Coordinates (h,               Pentagon.East,  Side.flip d)
        | Side.E -> Coordinates (Hex.southeast h, Pentagon.West,  Side.flip d)
    | Pentagon.West  ->
        match d with
        | Side.A -> Coordinates (h,               Pentagon.East,  Side.flip d)
        | Side.B -> Coordinates (h,               Pentagon.South, Side.flip d)
        | Side.C -> Coordinates (Hex.southwest h, Pentagon.North, Side.flip d)
        | Side.D -> Coordinates (Hex.northwest h, Pentagon.South, Side.flip d)
        | Side.E -> Coordinates (h,               Pentagon.North, Side.flip d)

実装をレコード型に変えたとしても、シグネチャには影響しません。

では、最後に、入力を受け取って結果を出力する部分を書いてみましょう。実は結構面倒くさいのですが、情報隠蔽にはあまり関係しないので、説明は省いて残りのコードだけ紹介します。ただし、一点だけ補足すると、「今まで通ってきたマス」をMap<_, _>型(イミュータブルな辞書クラス)に保存するために、Hex型のシグネチャにIComparableインターフェースを記述したのと、Coordinates型から「マス」として(Hex.Hex, Pentagon.Pentagon)型の情報を取り出す関数を追加します。

Hex.fsi
[<Sealed>]
type Hex =
    interface System.IComparable
Coordinates.fsi
val key: Coordinates -> Hex.Hex * Pentagon.Pentagon
Coordinates.fs
let key (Coordinates (h, p, _)) = (h, p)

さあ、残りのコードを記載してこの記事は終わりです。それではみなさん良いお年を。

Program.fs
type Result = (int * int) option
let move xs : Result =
    let rec move c i (m: Map<_, _>) xs : Result =
        match xs with
        | []    -> None
        | y::ys ->
            match y with
            | 'F' ->
                let c = Coordinates.flip c
                let i = i + 1
                let k = Coordinates.key c
                match (Map.tryFind k m) with
                | None    -> move c i (Map.add k i m) ys
                | Some v' -> Some (i, v')
            | 'c' -> move (Coordinates.clockwise c) i m ys
            | 'a' -> move (Coordinates.anticlockwise c) i m ys
            | _   -> move c i m ys // 例外を投げてもいい
    let c = Coordinates.origin
    let i = 0
    let k = Coordinates.key c
    let m = Map.add k i Map.empty
    move c i m xs

let exec (input: string) : string =
    match move (Seq.toList input) with
    | None        -> "-"
    | Some (m, n) -> sprintf "%d,%d" m n

let test (input: string) (expected: string) : unit =
    let actual = exec input
    if expected = actual then
        printfn "Success"
    else
        printfn "Fail"
        printfn "  expected: %A" expected
        printfn "  actual:   %A" actual

[<EntryPoint>]
let main argv =
    test "FcFcFaF" "3,0"
    test "FccFaaFcFcF" "-"
    test "ccFaF" "-"
    test "cFcFaaF" "-"
    test "aFccFcFcF" "4,1"
    test "cFaFaFaFaaF" "4,0"
    test "cFccFaFaaFaF" "5,1"
    test "cFaaFaaFcFcFccF" "5,2"
    test "aaFaFaaFaFccFaFccF" "6,0"
    test "aaFaaFcFccFcFccFccFaF" "6,1"
    test "aFccFccFaaFccFcFcFcFaF" "7,4"
    test "ccFaaFaaFaFccFaaFaFcFccFaaF" "-"
    test "aaFaFaFcFaFaaFaFaFaaFccFaaFaaF" "8,3"
    test "aaFccFaFaFccFaaFaaFaaFccFccFcFaFaF" "13,10"
    test "aFaFaaFaaFaaFccFaFccFaFaaFccFccFaaFccF" "11,0"
    test "ccFccFcFaaFaaFaFccFaaFaFcFaaFaaFcFcFcF" "10,2"
    test "aaFaaFaaFccFccFaFaaFaaFaFccFcFcFccFaaFaFccFccF" "10,3"
    test "ccFaFccFaaFaaFcFccFaFcFccFccFaaFccFaFaaFaFaFccF" "11,3"
    test "cFcFaFaaFaFccFaaFcFaFaaFccFaFaaFccFaFcFaaFccFcF" "12,0"
    test "cFaFaaFcFaaFaaFaFccFaaFcFaaFccFaaFccFaFcFccFaFaFaFcFcF" "19,16"
    test "aFaFccFaaFccFccFaFccFaFaaFccFaaFccFcFccFaFaFccFaaFccFcF" "15,11"
    test "aFccFccFccFaaFaaFcFcFaaFccFaFcFccFaaFaFcFaaFcFaFccFccFaFaF" "23,20"
    test "cFaFaaFcFaFaaFccFccFaaFaFccFccFccFaFaFccFccFccFaaFcFcFcFaF" "22,18"
    test "aFcFaFccFccFaFccFaaFaFaaFaaFaFcFaFcFcFcFaFaFaaFaaFaFaaFcFccFaF" "14,4"
    test "cFaFaaFcFaFccFcFccFaFaaFaaFaFcFccFccFaFcFaFaaFccFaFaFccFcFaFaFaaF" "22,19"
    test "aaFaaFaFcFccFaaFcFaaFccFaFaFcFaFaaFaFaFaFcFccFaFaaFcFcFccFccFccFcF" "16,11"
    test "cFaaFaFcFaFaaFccFaaFaFccFccFaaFccFcFaFaaFaaFaFccFcFccFccFaFaaFcFcFaF" "26,23"
    test "cFaaFccFaFcFaFcFaaFccFaFccFaaFcFaFaaFccFaaFccFcFaFaaFaFcFccFccFaFcFaaFccFcF" "-"
    test "ccFccFaaFaFccFaaFaaFcFccFaaFaFcFccFccFccFaFaaFcFaFccFaaFcFcFaFccFaFccFaaFaaFaF" "-"
    test "aaFccFaFccFccFaaFccFaFcFccFaFccFcFcFaFcFcFaaFaaFcFccFccFcFaFaaFaFcFcFaaFaaFaaF" "19,5"
    test "aFaaFccFaFaaFaaFccFccFaFcFaaFaaFaaFccFaFccFaaFaaFaFccFccFcFaaFaFccFccFaFcFaaFccF" "-"
    test "aaFaFaaFcFccFccFaFcFaaFaaFaFccFccFccFaFaaFccFcFaaFccFaaFccFaaFccFccFccFcFaaFcFaaFccF" "30,23"
    test "aaFccFaFaaFccFcFaFccFaFcFccFaaFccFaFcFaaFccFaaFaFccFccFccFcFccFaFccFcFaaFaFcFccFcFaFaaF" "32,17"
    test "ccFcFcFaFaaFaFaaFccFcFccFaFcFaFccFaaFcFccFcFaaFcFaaFcFaaFaaFcFaaFaFaFaFaaFccFaaFcFcFaFaF" "18,14"
    test "ccFaFaaFaaFcFaaFaaFaaFaaFaaFaFccFcFaaFaFcFccFaaFcFaFaaFccFccFaaFcFaaFaaFaaFccFaaFcFcFaFaFccFaFcFccF" "17,0"
    test "aaFccFccFaFcFaFcFaFccFccFccFaFccFccFcFaFcFaaFccFcFccFccFaaFcFccFaaFccFccFaFccFcFcFaaFaFccFcFaaFaaFcF" "24,11"
    test "aaFccFaFccFaaFaaFcFaFcFaFcFaaFaaFccFaFcFaFaaFcFaaFaFcFaaFccFcFaaFccFaaFccFaaFccFcFaFcFaFccFaaFaaFccFaaF" "-"
    test "aFaaFaFaaFcFccFaaFccFaFcFaaFccFccFcFcFaFaFcFccFcFccFaaFcFaFcFaaFcFaFaaFccFaaFccFaFaaFaFcFaFccFaFaaFaFaaF" "17,14"
    test "aaFcFaaFccFaaFaaFcFaaFccFccFaFcFaFcFccFcFaaFaaFaFcFaFcFaaFcFaFaaFaFccFccFccFcFaFaFcFcFaFcFaaFcFaaFcFcFaaF" "27,23"
    test "ccFaFccFaFcFaFccFcFaaFccFccFcFaFccFccFaaFaaFaFaaFcFaaFccFcFaaFcFccFaaFcFaFccFccFaFccFaaFaaFcFcFccFaFccFcFcF" "25,18"
    test "cFcFccFaFaaFaaFccFccFcFccFccFaFcFaaFcFccFccFcFccFaFaFaFaFaFaaFaFcFaFccFcFccFaaFccFaaFccFcFaFaaFccFaFccFcFaaF" "16,4"
    test "aFcFcFaaFcFaaFcFaFaaFaaFaaFaFcFccFccFccFaFaaFaFaaFaaFcFcFccFaFaaFcFaFaaFccFaaFcFccFcFcFccFaaFcFaFccFaaFaaFccF" "19,15"
    test "ccFaaFcFaFccFaFccFaFcFaaFaaFccFaaFccFcFaFaFccFaFaaFaFaaFaFaaFaaFccFaaFaFcFaaFaFaFcFaaFcFaFaaFaaFcFccFaaFaFaFaF" "22,17"
    test "aaFcFaaFaFcFccFccFaFaFccFaFaaFaFcFccFaaFaaFaaFcFccFccFaFaaFccFcFaFaFcFaaFcFcFcFccFccFaaFcFcFaFccFccFccFaaFcFaaFcF" "14,5"
    test "cFcFaFccFaaFaFccFaFccFaaFaFccFaaFaaFccFaaFccFaaFccFaFaaFaFaFaaFaaFccFaFcFaaFcFcFaFaFcFcFcFaFaaFcFaFcFaaFccFccFcFaaFaFccF" "10,1"
    test "ccFaFcFaaFcFccFaFccFaaFccFaaFaaFcFaaFccFaaFccFccFcFaaFaaFcFaaFccFaaFcFcFaaFaFaFccFcFaFaFaFccFccFccFccFccFccFcFcFccFccFcF" "25,13"
    test "aFccFaFcFaFcFaaFaaFcFaaFccFaFaaFaFcFaFaaFaaFaaFccFaaFaaFccFccFccFccFccFaaFaFcFaFaFccFaFccFcFccFcFcFccFcFccFccFccFccFccFaF" "18,8"
    test "cFaaFcFaaFaFcFccFaaFcFccFcFaFccFaaFaaFccFccFccFaFcFccFcFaaFcFcFccFcFccFaFaFccFccFcFaFaFaaFcFaFcFccFccFccFaaFaFaFccFcFccFccF" "25,22"
    test "aaFcFccFcFaFcFaFaFcFccFaFaaFaFcFccFccFaaFccFccFaaFaaFccFaaFaaFccFaaFccFcFaaFaFcFaFcFccFaaFccFaFaFccFaaFcFaaFaFccFaaFaaFccFccFcFaF" "-"
    test "aFaaFccFccFaaFaaFccFccFaaFaaFaFcFccFaFcFaaFaFccFaaFccFaFcFaFccFaaFaaFaFccFaFaaFaaFcFaaFccFaaFcFaFccFccFaaFaaFaaFaaFccFcFccFaaFaFaaFaaF" "-"
    test "aaFcFccFccFaFccFaFccFaaFaaFaaFccFaaFaaFccFaaFccFaFaaFccFccFaaFcFccFaFccFcFaaFaFccFccFccFaaFaFccFccFaFaaFccFccFccFaaFccFccFaFaFcFaFccFccF" "-"
    0
2
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
2
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?