LoginSignup
2

More than 3 years have passed since last update.

F#でのOOP

Posted at

F#でのOOP

F#ではOOPも扱える。
そして判別共用体とか関数型言語としての機能も持ち合わせているため
デザパタで作らないといけないひな形コードをあまり書かなくてもよかったりする。
例えばCommandパターンは高階関数に置き換えられたり。

F#のクラス

.NETファミリーなので、オブジェクトの同一性が担保されている。
つまり、あらゆるものがSystem.Objectのインスタンスとして扱われる。
(F#ではobjと省略可能)

以下の関数はみんな持っている事を覚えておくとよい。

  • ToString()
  • GetHashCode()
  • Equals()
  • GetType()

クラス

参照同値性

その上で、オブジェクトの比較は値型と参照型で挙動が異なる。
参照型は参照同値性(メモリアドレス)比較となるので、直感的ではない。
そのため、必要に応じてEquals()をオーバーライドする。
Equals()をオーバーライドすると、セットでGetHashCode()も定義しないといけないので注意する。

F#ではタプル、判別共用体、レコード型も本当は参照型だけど
コンパイラがEquals()GetHashCode()を勝手にオーバーライドしてくれるので
普通に=で比較ができたりする。

Equals
GetHashCode

コンストラクタ

基本的には暗黙的なコンストラクタで良いとは思う。

複数のコンストラクタを提供したい時や、後からプロパティの値を決めたい場合は
明示的なコンストラクタで書くと良いのかも。

letとvalは後から値を決めるかどうかで使い分ける。

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"

オーバーロード

型推論とかジェネリック型で楽できるかなーと思ったけどそうはいかなかった。
地道に定義するしかないみたい。

intとfloat版を作成
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になる。

インターフェース

シグネチャ

NewType.fsi
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
NewType.fs
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
Program.fs
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だけ定義しようとしたらコンパイルエラーになる。

以下の例だとあんまりシグネチャの意味はないかも。

継承

NewType.fsi
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
NewType.fs
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
Program.fs
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は生成できない
[<AbstractClass>]
type SealedSword() =
    abstract member Slash : float * float -> float

    abstract Broken: bool with get, set

// 抽象クラスは生成できないのでコンパイルエラー
let roy = SealedSword()

AbstractClassを指定しない場合
// 実装がないので型のコンパイルエラーになる
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"


キャスト

キャストと変換

Eliwood?知らない人ですね…
[<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

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