かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

手軽なスクリプト言語としてのF# その11「インターフェースと演算子のオーバーロード」

そろそろ、関数言語っぽいことしたいけど、まだもうちょっとかかります。

インターフェース

インターフェースです!インターフェースは、全てabstractなメンバーで構成される型だとインターフェースになるみたいです。

type IFooable =
  abstract Foo : (string * string) -> string
  abstract Bar : unit -> unit

[]をつけると抽象クラスになるみたいです。インターフェースの実装は抽象クラスとは違って以下のような形で実装します。

type Foo() =
  interface IFooable with
    member this.Foo ((s1, s2)) = "hogehoge"
    member this.Bar() = printfn "sample"

F#のインターフェースの実装は、C#でいう明示的な実装なので以下のように書いてもメソッドを呼ぶことは出来ません。

let f = Foo()
f.Bar() // コンパイルエラー!

なのでインターフェースにキャストするか、インターフェースのメソッドと同じメソッドを作ってあげる必要があります。因みにアップキャストは、:>でやってダウンキャストは:?>で出来ます。

let f = Foo()
(f :> IFooable).Bar() // キャストしてメソッドを呼ぶ

あとは、Fooクラスにインターフェースと同名のメソッドを定義してやる方法もあります。

type IFooable =
    abstract Foo : (string * string) -> string
    abstract Bar : unit -> unit

type Foo() =
    interface IFooable with
        member this.Foo ((s1, s2)) = "hogehoge"
        member this.Bar() = printfn "sample"

    // インターフェースのメソッドと同名のメソッドを定義する
    member this.Bar() = (this :> IFooable).Bar()

let f = Foo()
f.Bar()

演算子のオーバーロード

さて、F#では演算子も好きなように定義出来ます。
演算子のオーバーロードはstaticメソッドの定義と同じ要領で出来ます。staticメソッドの定義はやってませんが、staticつけるだけな上にクラス名.メソッド名でアクセスするのもC#と同じです。例としてint型の値を保持するHolderクラスを定義して、足し算が出来るようにしてみます。

// 値を保持するだけのクラス
type Holder(value) =
    let value : int = value

    member this.Value with get() = value

    // メソッド名は括弧で演算子を囲ったもの
    static member (+) (lhs : Holder, rhs : Holder) =
        // 足した結果を返すよ
        Holder(lhs.Value + rhs.Value)

// 足し算してみる
let a = Holder(10)
let b = Holder(3)
let c = a + b
// 結果出力
printfn "%d" c.Value

結果は13と表示されます。演算子は、かなり自由に定義できるようになってます。一覧はMSDNを参照してください。

最後にちょっと長いサンプル

さて、今日はインターフェースだけの説明にしようと思ってたのですが何故か急に演算子のオーバーロードもやってしまいました。理由は、インターフェースを使ったサンプルプログラムを書こうと思ってて何かC#でいい題材が無いかと思ってC# インターフェースでBingって一番上に出たものにしようと思ったらこれだったわけですよ。長い上に演算子のオーバーロードまでやってはる…orz
ということで演算子のオーバーロードもやったので、上記のページ(ufcppさんのページね)のサンプルをF#で焼き直してみようと思います。
とりあえず、今までやった範囲内の機能を使って実装してみるので、関数型のうまみは出てないはずです。(サンプルでは構造体使ってますが、構造体は、ここで作ったサンプルでは使ってないです)

open System

// 二次元の点を表す
type Point(x : float, y : float) =
    let mutable x = x
    let mutable y = y

    // X座標
    member this.X with get() = x and set(v) = x <- v
    // Y座標
    member this.Y with get() = y and set(v) = y <- v

    // ベクトル和
    static member (+) (a : Point, b : Point) =
        Point(a.X + b.X, a.Y + b.Y)
    
    // ベクトル差
    static member (-) (a : Point, b : Point) =
        Point(a.X - b.X, a.Y - b.Y)

    // A-B間の距離を求める
    static member GetDistance(a : Point, b : Point) =
        let x = a.X - b.X
        let y = a.Y - b.Y
        Math.Sqrt(x ** 2. + y ** 2.)

    override this.ToString() =
        sprintf "(%f, %f)" x y

// 2次元空間上の図形
type Shape =
    abstract GetArea : unit -> float
    abstract GetPerimeter : unit -> float 

// 2次元空間上の円をあらわすクラス
type Circle(center : Point, r : float) =
    let mutable center = center
    let mutable radius = r

    // 円の中心
    member this.Center with get() = center and set(v) = center <- v
    // 円の半径
    member this.Radius with get() = radius and set(v) = radius <- v

    // インターフェースの実装
    interface Shape with
        member this.GetArea() = Math.PI * radius ** 2.
        member this.GetPerimeter() = 2. * Math.PI * radius

    override this.ToString() = sprintf "Circle (c = %A, r = %f" center radius

// 2次元上の三角形を表すクラス
type Triangle(a : Point, b : Point, c : Point) =
    let mutable a = a
    let mutable b = b
    let mutable c = c

    // 頂点A
    member this.A with get() = a and set(v) = a <- v
    // 頂点B
    member this.B with get() = b and set(v) = b <- v
    // 頂点C
    member this.C with get() = c and set(v) = c <- v

    // インターフェースの実装
    interface Shape with
        member this.GetArea() =
            let ab = b - a
            let ac = c - a
            0.5 * Math.Abs(ab.X * ac.Y - ac.X * ab.Y)

        member this.GetPerimeter() =
            let l = Point.GetDistance(a, b)
            let l = l + Point.GetDistance(a, c)
            l + Point.GetDistance(b, c)
    
    override this.ToString() =
        sprintf "Circle (a = %A, b = %A, c = %A)" a b c

// 自由多角形を表すクラス
type Polygon(verteces : Point array) = 
    let mutable verteces = verteces

    // 頂点の集合
    member this.Verteces with get() = verteces and set(v) = verteces <- v

    // インターフェースの実装
    interface Shape with
        member this.GetArea() =
            let mutable area = 0.
            let mutable p = verteces.[verteces.Length - 1]
            for q in verteces do
                area <- area + p.X * q.Y - q.X * p.Y
                p <- q
            0.5 * Math.Abs(area)

        member this.GetPerimeter() =
            let mutable perimeter = 0.
            let mutable p = verteces.[verteces.Length - 1]
            for q in verteces do
                perimeter <- perimeter + Point.GetDistance(p, q)
                p <- q
            perimeter


    override this.ToString() =
        let sb = System.Text.StringBuilder()
        sb.AppendFormat("Polygon({0}", verteces.[0]) |> ignore
        for p in verteces do
            sb.AppendFormat(", {0}", p) |> ignore
        sb.Append(")") |> ignore
        sb.ToString()

let Show (f : Shape) =
    printfn "図形 %A" f
    printfn "面積/周 = %f" (f.GetArea() / f.GetPerimeter())


let t = Triangle(Point(0., 0.), Point(3., 4.), Point(4., 3.))
let c = Circle(Point(0., 0.), 3.)
let p1 = Polygon([|Point(0., 0.); Point(3., 4.); Point(4., 3.)|])
let p2 = Polygon([|Point(0., 0.); Point(0., 2.); Point(2., 2.); Point(0., 2.); Point(2., 2.)|])

// 多態!!
Show(t)
Show(c)
Show(p1)
Show(p2)

実行結果が正しいのかどうかわかりませんが、実行してみました。

図形 Circle (a = (0.000000, 0.000000), b = (3.000000, 4.000000), c = (4.000000,
3.000000))
面積/周 = 0.306635
図形 Circle (c = (0.000000, 0.000000), r = 3.000000
面積/周 = 1.500000
図形 Polygon((0.000000, 0.000000), (0.000000, 0.000000), (3.000000, 4.000000), (
4.000000, 3.000000))
面積/周 = 0.306635
図形 Polygon((0.000000, 0.000000), (0.000000, 0.000000), (0.000000, 2.000000), (
2.000000, 2.000000), (0.000000, 2.000000), (2.000000, 2.000000))
面積/周 = 0.184699

まとめ

ちょっと長いプログラムを書きましたが、基本的にC#とほぼ1対1に対応する感じになっています。なので、とりあえずC#っぽい言語としてF#を使えるようになってきたころかな?