次期 Go 言語で導入される(かもしれない)総称型について予習する

【2021-02-21 追記】 型パラメータの提案(「総称型」というのは厳密には正しくないらしい)は2021年2月に承認され,製造フェーズに入ったようだ。
  1. 次期 Go 言語で導入される(かもしれない)総称型について予習する ←イマココ
  2. 次期 Go 言語で導入される総称型について予習する(その2)
  3. 次期 Go 言語で導入される総称型について予習する(その3)

2018年8月に次期 Go 言語で追加される(かもしれない)仕様についてアナウンスがあった。

“Go 2” といってもメジャー・バージョンが変わるのではなく現行バージョンに対する追加仕様らしい。 したがって後方互換性は確保されているようだ。

紹介されているドラフト案は大きく2つある。

このうち今回は総称型について予習してみる。

なお “Go 2” の提案はまだドラフト段階なので大幅に変更になったり場合によっては立ち消えになる可能性もある。 なので,この記事では深いところまで踏み込まずフワっとした説明になるけど,あしからずご了承の程を。

総称型のメリット

ソフトウェア・エンジニアには自明だと思うが,まずは復習から。

具体例として2つの値のうち大きい方を返す関数を考えてみる。

package main

import "fmt"

func max(x, y int) int {
    if x < y {
        return y
    }
    return x
}

func main() {
    x := 1
    y := 2
    fmt.Printf("max(%v, %v) = %v\n", x, y, max(x, y))
    //Output
    //max(1, 2) = 2
}

この関数 max() は int 型で記述しているが byte 型や float32/float64 型でも関数の中身は全く同じコードになる。

package main

import "fmt"

func max(x, y float64) float64 {
    if x < y {
        return y
    }
    return x
}

func main() {
    x := 1.1
    y := 1.2
    fmt.Printf("max(%v, %v) = %v\n", x, y, max(x, y))
    //Output
    //max(1.1, 1.2) = 1.2
}

ならば,最初から汎化した型で単一のコード記述すれば型ごとに複数のコードを量産しなくてもいんじゃね? という発想になる1。 これが総称型の原点である。

たとえば Java で総称型を使うと以下のような記述になる2

public static <T> T max(T x, T y) {
    ...
}

<T> の部分が総称型の定義にあたる。 ちなみに総称型の名前(この場合は T)はスコープ内で被らなければ任意に指定できる。

現行版 Go 言語において総称型の不在で割りを食っている典型例のひとつが sort パッケージで,基本型の slice のソートだけで以下の型が用意されている3

後方互換性を確保するため,これらの型がなくなることはないだろうけど,総称型が実現すれば内部実装の refactoring が進むかも知れない。

このように総称型はコンテナ(container; オブジェクトの集合を表現するデータ構造)操作で特に威力を発揮する。 また,現行版 Go 言語では総称型を用いずとも interface 型と reflect パッケージを使ってかなりの部分を代替できるが,コード量のコスト高を別にしても,記述の正しさは実行時での評価ではなくコンパイル時に評価して欲しいところである。

そういうわけで,今までずうっと後回しにされてきたが,総称型を導入できるのであれば是非とも期待したいものである。

型パラメータ(Type Parameter)と型引数(Type Argument)

次期 Go 言語で総称型を導入するために型パラメータおよび型引数の構文を追加するようだ。

例えば先程の max() 関数であれば以下のように記述できる。

package main

import "fmt"

func max(type T)(x, y T) T {
    if x < y {
        return y
    }
    return x
}

func main() {
    x := 1
    y := 2
    fmt.Printf("max(%v, %v) = %v\n", x, y, max(x, y))
    //Output
    //max(1, 2) = 2
}

(type T) の部分が型パラメータで,これによって総称型を定義している。 既存の語彙だけで構成しているのがポイント(<> は演算子だし [...] は 配列/slice や map の構文で使われるので避けたのだろう)。

関数を呼び出す側は型推論によって引数の型が一意に決定するので特別な記述は必要ない。 相変わらず refatoring に優しい言語だよな(笑)

明示的に型を指定するなら

max(int)(x, y)

などと記述する。 (int) の部分が型引数にあたる。

型宣言とインスタンス生成でも同様に

type List(type T) []T

var listInt = List(int){0, 1, 2, 3, 4 ,5}

などと記述できる。

型コントラクト(Type Contract)

先程の max() 関数だが

func max(type T)(x, y T) T {
    if x < y {
        return y
    }
    return x
}

T のインスタンス同士で比較演算(具体的には x < y)が可能である必要がある。 たとえば complex64/complex128 は基本型だが < 演算子による比較ができない。

Java の場合は継承を構成して

public static <T extends Comparable<? super T>> T max(T x, T y) {
    ...
}

などと記述することで総称型に対する制約(type constraint)を表現できる4

しかし Go 言語ではいわゆる「継承」の仕組みを持ってないため別のアプローチをとる必要がある。 それが型コントラクトである。 型コントラクトでは contract キーワードおよびそれを使った構文を追加する。 具体的には以下のようなコードになる。

contract comparable(t T) {
    t < t
}

func max(type T comparable)(x, y T) T {
    if x < y {
        return y
    }
    return x
}

なお comparable の型引数を明示する場合は

func max(type T comparable(T))(x, y T) T { ... }

と書く。

型コントラクトの記述はバイナリ・コードにコンパイルされない。 上の例では型 T に対して比較演算子 < が使えることを要求しているとコンパイラに知らせるものである。 これなら T を complex128 に展開しようとしてもコンパイル時に「契約違反」になるわけだ。

型コントラクトは型コントラクトに埋め込むことができる。 例えばこんな感じ。

contract equalable(t T) {
    t == t
}
contract comparable(t T) {
    equalable(T)
    t < t
}

これで comparable== 演算子と < 演算子が使えることを要求していることになる。

継承を利用した制約と異なり,型コントラクトの自由度は高く応用範囲も広い。 たとえばポインタ型を要求するなら

contract pointer(t T) {
    *t
}

などと書くこともできるらしい。 他にも io.Reader と汎化・特化の関係があることを要求するなら

contract readable(r T) {
    io.Reader(r)
}

などと書けばいいようだ。

もし今回のドラフト案の通りに総称型が実現するなら型コントラクトの整備が喫緊の作業となるだろう。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-05-21)
Kindle版
B094PRR7PZ (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. このように同じコードを重複させないように記述するコーディング指針を「OAOO (Once And Only Once) 原則」と呼ぶ。そういえばよくある勘違いで「DRY (Don’t Repeat Yourself) 原則」と解説している記事が見受けられるが, DRY 原則は同じ意味を持つ情報やデータを複数に散らばせないというシステム設計や開発環境の指針を指すものである。 ↩︎

  2. 久しぶりに Java コード書いたら型を前置することに違和感がハンパない。型の前置なんて非合理的だよなあ(もちろん偏見w) ↩︎

  3. Slice のソートについてはバージョン 1.8 から任意の型に対応する sort.Slice() 関数が用意された。内部で reflect パッケージを使っているが,かなり巧妙に組まれているため,パフォーマンス低下は殆どないらしい。ただし slice 以外のインスタンスを指定すると(コンパイル時ではなく)実行時に panic を吐く。詳しくは「ソートを使う」を参照のこと。 ↩︎

  4. C++ や C# でも where 句を用いて総称型に対する制約を表現できるが,基本は継承を利用したものである。 ↩︎