この記事は、F# Advent Calendar 2016の24日目に間に合わなかった記事です。(ギリギリ年内になりました。)
情報隠蔽の概要
「インターフェースと実装の分離」や、より広く「情報隠蔽」などと言われるものは、多くのプログラミング言語で実現可能です。たとえばC#やJavaでは、interface
とclass
を使い分けたり、クラスの中でもpublic
やprivate
などといったアクセス修飾子でアクセシビリティを制御したりします。もちろんF#でも同じことができます。
module Module1
type internal MyInternalType () =
let x = 5 // クラス内のlet束縛はデフォルトでprivate
member private this.X() = 10
member this.Z() = x * 100
F#ではlet
束縛でモジュールに値や関数を定義することができますが、こちらにもアクセス修飾子をつけることができます。
module Module1
let internal myInternalObj = new MyInternalType()
let private add x y = x + y
さて、F#にはレコード型や判別共用体という便利な型もあるのですが、これらの情報隠蔽はどうなるでしょうか。
レコード型や判別共用体は、パターンマッチを便利に使うことができますが、それは内部実装をすべて公開しているからこそです。レコード型については、フィールドは自動的にプロパティとして公開されますし、インスタンスを初期化するときには全フィールドに何らかの値を設定することになります。一部のフィールドを非公開にすることもできませんし、デフォルトのコンストラクタをprivate修飾して独自のコンストラクタのみを使わせることもできません。また、判別共用体については、すべてのコンストラクタ(識別子)が公開されます。一部のコンストラクタを非公開にしたいと思っても、そうする方法はありません。
では、あるモジュールにレコード型や判別共用体を定義してそれをモジュール外に公開したときに、モジュール内ではでパターンマッチを使いつつ、モジュール外ではその型の内部実装にアクセスできずパターンマッチもできないようにすることは可能でしょうか?答えはイエスです。それには、シグネチャファイルを使います。
たとえばモジュールの実装がこうだったとします。
module Module1
type Type1 = { X: int; Y: int; }
let value1 = { X=1; Y=2; }
type Type2 = A | B
let value2 = A
このファイルと対になるシグネチャファイル(拡張子 .fsi) を用意します。
module Module1
type Type1
val value1: Type1
type Type2
val value2: Type2
すると、このモジュールの外からは、シグネチャファイルに書かれた情報しか参照できません。モジュールに型 Type1
と Type2
が定義されていることはわかりますが、これらの実装詳細は隠されるため、レコード型や判別共用体であることはわかりません。したがって、モジュール外ではvalue1
からパターンマッチでフィールドの値を取り出したり、value2
からパターンマッチで処理を分岐させることもできません。
ただ、このように書くと、Type1
やType2
が何らかのインターフェースを実装しているかどうかも外部からはわからなくなってしまいます。実際、レコード型や判別共用体は自動的にIEquatable<_>
, IStructuralEquatable
, IComparable
, IStructuralComparable
といったインターフェースを実装しますが、それが外部からは見えなくなるため、equality
制約やcomparison
制約が要求されるジェネリック型にType1
やType2
をパラメーターとして渡すことができなくなってしまいます。それだと困る場合は、シグネチャファイルを次のように書きます。
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つまとめると六角形になり、それが平面を埋め尽くしていることに気づきます。
であれば、「どの五角形の上にいて、どの辺を向いているのか」は、「どの六角形の上にいるか」+「六角形の中にある五角形のどれか」+「どの辺を向いているのか」という3つの表現に分割できます。
「どの六角形の上にいるか」は、直行しないx-y座標軸をとることで、2つの整数の組で表すことができそうです。
「六角形の中にある五角形のどれか」は、4通りが考えられます。ここでは東西南北(North, East, South, West)であらわすことにします。
そして最後に、「どの辺を向いているのか」です。これは五角形ですから5通りが考えられます。それぞれa, b, c, d, eとしましょう。どの辺にどの記号を割り当てるかは自由に決められますが、ここではNorthとSouth、EastとWestが向かい合わせになっている辺をaとして、そこから時計回りにb, c, d, eと割り振ります。
ではF#の型に落としてみましょう。「どの六角形の上にいるか」をHex
モジュールのHex
型、「六角形の中にある五角形のどれか」をPentagon
モジュールのPentagon
型、「どの辺を向いているのか」をSide
モジュールのSide
型に定義します。そしてそれら3つをまとめたものをCoordinates
モジュールのCoordinates
型としましょう。
module Hex
type Hex = {X:int; Y:int}
module Pentagon
type Pentagon = North | East | South | West
module Side
type Side = {I:int}
module Coordinates
type Coordinates = Coordinates of Hex * Pentagon * Side
Side
型は5択なので判別共用体で定義してもいいのですが、あえて数値をラップした型として定義してみました。Coordinates
型はレコード型でもいいのですが、ちょっとひねって、単一の判別共用体でタプルをラップしてみました。
さて、これをベースに、シグネチャファイルを用意して情報隠蔽しながら、実装を膨らましてみましょう。
まずはHex
型。レコード型であることを隠蔽すると、外部からはパターンマッチもコンストラクタも使えませんので、モジュールに即値や生成関数を定義してやります。初期値を起点として、辺をまたぐ形でマス目を移動するわけですから、シグネチャはこんな感じでしょうか。
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
実装はこうです。
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であることと、時計周り/反時計回りのロジックを持たせることができます。また、次の図をよく見ると、向かい合わせになっている辺に法則があることがわかります。
それは、aとaが向かい合わせになるのは最初の定義の通りですが、それ以外は、bとc、dとeが常に向かい合わせになっています。これもロジックとして抜き出しておきましょう。
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>
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
モジュールのアクティブパターンを使います。)
module Coordinates
type Coordinates
val origin: Coordinates
val clockwise: Coordinates -> Coordinates
val anticlockwise: Coordinates -> Coordinates
val flip: Coordinates -> Coordinates
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)
型の情報を取り出す関数を追加します。
[<Sealed>]
type Hex =
interface System.IComparable
val key: Coordinates -> Hex.Hex * Pentagon.Pentagon
let key (Coordinates (h, p, _)) = (h, p)
さあ、残りのコードを記載してこの記事は終わりです。それではみなさん良いお年を。
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