F#でのOOP
F#ではOOPも扱える。
そして判別共用体とか関数型言語としての機能も持ち合わせているため
デザパタで作らないといけないひな形コードをあまり書かなくてもよかったりする。
例えばCommandパターンは高階関数に置き換えられたり。
F#のクラス
.NETファミリーなので、オブジェクトの同一性が担保されている。
つまり、あらゆるものがSystem.Objectのインスタンスとして扱われる。
(F#ではobjと省略可能)
以下の関数はみんな持っている事を覚えておくとよい。
- ToString()
- GetHashCode()
- Equals()
- GetType()
参照同値性
その上で、オブジェクトの比較は値型と参照型で挙動が異なる。
参照型は参照同値性(メモリアドレス)比較となるので、直感的ではない。
そのため、必要に応じてEquals()
をオーバーライドする。
Equals()
をオーバーライドすると、セットでGetHashCode()
も定義しないといけないので注意する。
F#ではタプル、判別共用体、レコード型も本当は参照型だけど
コンパイラがEquals()
とGetHashCode()
を勝手にオーバーライドしてくれるので
普通に=で比較ができたりする。
コンストラクタ
基本的には暗黙的なコンストラクタで良いとは思う。
複数のコンストラクタを提供したい時や、後からプロパティの値を決めたい場合は
明示的なコンストラクタで書くと良いのかも。
letとvalは後から値を決めるかどうかで使い分ける。
type Color = Yellow | Red | Blue | Green | Purple
// 暗黙的なコンストラクタ
type CardLet(color:Color, number:int) =
let cardColor = color
let cardNumber = number
// クラスにくっつくのはメソッドという(関数とは別モノ)
// 引数なしの場合はUnitを引数に取るものとする
member x.Color with get() = cardColor
member x.Number with get() = cardNumber
// 明示的なコンストラクタ
type CardVal =
val cardColor : Color
val cardNumber : int
new(color, number) = {
cardColor = color;
cardNumber = number;
}
member x.Color with get() = x.cardColor
member x.Number with get() = x.cardNumber
// new つけてもいいけど冗長
CardLet(Red, 1) |> printfn "%A"
CardVal(Red, 1) |> printfn "%A"
メソッドの定義の仕方
F#の関数と同じ方式でも書けるけど.Net的には非推奨。
タプルにすると他言語から見たときの互換性がある。
type NewType() =
// F#関数形式だと部分適用やカリー化できる
// でも.Netの他言語が対応してないので非推奨
member x.PrintFsharp a b = printfn "%A%A" a b
// タプルで定義すると他言語から見たときの互換性がある
member x.PrintDotnet(a, b) = printfn "%A%A" a b
let g = NewType()
g.PrintFsharp 5 6
g.PrintDotnet(5, 6)
関数とメソッドの違い
明示的にrecつけなくても再帰ができるかどうかが決定的な違い。
// 関数はrecを明示的につけないとコンパイルエラー
let recFunction a b =
if a = 10 then
b
else
recFunction (a + 1) (a + b)
type NewType() =
// メソッドはrecつけなくても普通に再帰できる
member x.RecMethod(a, b) =
if a = 10 then
b
else
x.RecMethod(a + 1, a + b)
// F#関数スタイルでも再帰はできる
member x.RecFunction a b =
if a = 10 then
b
else
x.RecFunction (a + 1) (a + b)
let neet = NewType()
neet.RecMethod(1, 0) |> printfn "%A"
neet.RecFunction 1 0 |> printfn "%A"
オーバーロード
型推論とかジェネリック型で楽できるかなーと思ったけどそうはいかなかった。
地道に定義するしかないみたい。
type NewType() =
// オーバーロード
member x.add(a:int, b:int) =
a + b
member x.add(a:float, b:float) =
a + b
let neet = NewType()
neet.add(1, 0) |> printfn "%A"
neet.add(1.00, 0.01) |> printfn "%A"
以下はちょっとした実験
type NewType() =
// 型修飾つけなければオーバーロード実装しなくてもいけるのでは?
member x.add(a, b) =
a + b
let neet = NewType()
// これによってaddはintを受け取るものと定義される
neet.add(1, 0) |> printfn "%A"
// addはintしか受け取らないので型エラー
neet.add(1.00, 0.01) |> printfn "%A"
// ジェネリック型にしても無理
type NewType() =
member x.add(a:'a, b:'a) =
a + b
let neet = NewType()
neet.add(1, 0) |> printfn "%A"
neet.add(1.00, 0.01) |> printfn "%A"
アクセス修飾子
private、public、internalの3種のみ。
protectedはない。
type NewType() =
// クラス内でのみ参照可能
member private x.Add(a, b) =
a + b
// どこからでも見える デフォルトはこれらしい
member public x.Sub(a, b) =
a - b
// 同一アセンブリからのみ参照可能
member internal x.Mul(a, b) =
a * b
let neet = NewType()
// Addはprivateなので参照できない
neet.Add(1, 0) |> printfn "%A"
neet.Sub(5, 2) |> printfn "%A"
neet.Mul(3, 3) |> printfn "%A"
シグネチャとインターフェース
一緒くたに並べてるけど、それぞれ別の概念である事に注意。
インターフェースは定義情報だけ書いて実装を分離する。
アクセス修飾子もここでまとめて書けるけど、一個一個定義するのは大変だったりする。
シグネチャ(*.fsi)は列挙したものだけ、外から見えるようになる。
シグネチャ定義に書いたけどアクセス修飾子がないものは、全部publicになる。
シグネチャ定義に書いてないけど実装だけあるものは、一律privateになる。
namespace Test
// クラス単位でアクセス修飾子を指定できる
type public NewType =
// 暗黙コンストラクタもシグネチャ作るなら定義しとかんと呼べない
new : unit -> NewType
// メソッド単位でアクセス修飾子をつけることもできる
// 引数と戻り値は型定義だけ書いてあげる必要があるみたい
member internal Add : int * int -> int
member internal Sub: int * int -> int
member public Mul: int * int -> int
namespace Test
type NewType() =
member x.Add(a, b) =
a + b
member x.Sub(a, b) =
a - b
member x.Mul(a, b) =
a * b
open Test
let neet = NewType()
neet.Add(1, 0) |> printfn "%A"
neet.Sub(5, 2) |> printfn "%A"
neet.Mul(3, 3) |> printfn "%A"
継承
大事なのはabstractとdefaultの対を同一ファイルに記載すること。
abstractがシグネチャにあるからといって
実装クラスにdefaultだけ定義しようとしたらコンパイルエラーになる。
以下の例だとあんまりシグネチャの意味はないかも。
namespace Test
type public NewType =
new : unit -> NewType
// 親クラスにはabstractつけとかないと子クラスでオーバーライドできない
abstract member Add : int * int -> int
abstract member Sub: int * int -> int
// こいつはオーバーライド不可
member Mul: int * int -> int
namespace Test
type NewType() =
// シグネチャにも定義してるけど
// 継承のためには結局実装とセットで書かないといけない
// abstract と defaultの対が必要である
abstract member Add : int * int -> int
default x.Add(a, b) =
a + b
abstract member Sub: int * int -> int
default x.Sub(a, b) =
a - b
member x.Mul(a, b) =
a * b
open Test
type Neet() =
inherit NewType()
override x.Add(a, b) =
a + b + 1
override x.Sub(a, b) =
a - b - 1
type Tiffa() =
inherit NewType()
override x.Add(a, b) =
a + b + 2
override x.Sub(a, b) =
a - b - 2
let nt = NewType()
let neet = Neet()
let tiffa = Tiffa()
nt.Add(5, 2) |> printfn "%A"
nt.Sub(5, 2) |> printfn "%A"
neet.Add(5, 2) |> printfn "%A"
neet.Sub(5, 2) |> printfn "%A"
tiffa.Add(5, 2) |> printfn "%A"
tiffa.Sub(5, 2) |> printfn "%A"
// Mulももちろん呼べるが、overrideはできない
tiffa.Mul(5, 2) |> printfn "%A"
抽象クラス
継承必須にしたい場合。
[<AbstractClass>]
を明示すると、そのクラスの生成をしようとするとコンパイルエラーにしてくれる。
親が持っているabstractなものは全部overrideしないとコンパイルエラーになる。
[<AbstractClass>]
type SealedSword() =
abstract member Slash : float * float -> float
abstract Broken: bool with get, set
type Roy() =
inherit SealedSword()
let mutable swordIsBroken = false
override x.Slash(baseDamage, magnification) =
baseDamage * magnification
override x.Broken
with get() = swordIsBroken
and set(broken:bool) = swordIsBroken <- broken
let roy = Roy()
roy.Broken |> printfn "%A"
roy.Slash(100.0, 1.2) |> printfn "%A"
roy.Broken <- true
roy.Broken |> printfn "%A"
[<AbstractClass>]
type SealedSword() =
abstract member Slash : float * float -> float
abstract Broken: bool with get, set
// 抽象クラスは生成できないのでコンパイルエラー
let roy = SealedSword()
// 実装がないので型のコンパイルエラーになる
type SealedSword() =
abstract member Slash : float * float -> float
abstract Broken: bool with get, set
// [<AbstractClass>]がないのでここはコンパイルエラーにならない
let roy = SealedSword()
シールクラス
継承を禁止したい場合。
副次的な効果として、[<Sealed>]
を指定する事で若干の最適化が行われるらしい。
MSDOCで資料見つけられなかった。
ここに書いてる説明はオライリー本の情報。
関係ないけどSealedって封印って意味か。かっこいい。
[<Sealed>]
type SealedSword() =
abstract member Slash : float * float -> float
abstract Broken: bool with get, set
// 封印の剣は封印されているのでコンパイルエラー
type Roy() =
inherit SealedSword()
let mutable swordIsBroken = false
override x.Slash(baseDamage, magnification) =
baseDamage * magnification
override x.Broken
with get() = swordIsBroken
and set(broken:bool) = swordIsBroken <- broken
let roy = Roy()
roy.Broken |> printfn "%A"
roy.Slash(100.0, 1.2) |> printfn "%A"
roy.Broken <- true
roy.Broken |> printfn "%A"
キャスト
[<AbstractClass>]
type SealedSword() =
abstract member Slash : float * float -> float
abstract Broken: bool with get, set
override x.ToString() = sprintf "SealedSword broken:%b" x.Broken
type Roy() =
inherit SealedSword()
let mutable swordIsBroken = false
override x.Slash(baseDamage, magnification) =
baseDamage * magnification
override x.Broken
with get() = swordIsBroken
and set(broken:bool) = swordIsBroken <- broken
let roy = Roy()
// 静的アップキャスト(子⇒親)
// 子のRoyを親のSealedSwordにキャスト
roy :> SealedSword |> printfn "%A"
// 一旦始祖のobjに変換
let objectRoy = roy :> obj
// 動的ダウンキャスト(親⇒子)
// 始祖のobjから子のRoyにキャスト
// 失敗したらSystem.InvalidCastExceptionになる
objectRoy :?> Roy |> printfn "%A"
動的ダウンキャストの時、いちいち例外処理入れるのはコストがかかるので
パターンマッチをかませると良い。
// xはobj型だけど、各パターンでas ~ にしてる事でその型の値として扱う事ができる
let whatIs (x:obj) =
match x with
| :? Roy as r -> printfn "%A is Roy" r
| :? SealedSword as ss -> printfn "%A is SealedSword" ss
| _ -> printfn "%A" <| x.GetType().Name
要注意なのは、子クラスから順番に定義しないと到達不能コードになるので注意。
// RoyはSealedSwordの子なので、whatisにRoyを渡すと全部SealedSwordにマッチしてしまう。
let whatIs (x:obj) =
match x with
| :? SealedSword as ss -> printfn "%A is SealedSword" ss
| :? Roy as r -> printfn "%A is Roy" r
| _ -> printfn "%A" <| x.GetType().Name