🤔

Interface 型をあらかじめ宣言しなくてもよい

2021/11/20に公開

いつもの小ネタです。起点は以下の tweet から。

https://twitter.com/mattn_jp/status/1461887274744905728

かいつまんで説明すると,元々の tweet

golang、interface Aとinterface Bを満たすものを引数として受け取れる関数を表現するのにinterface ABを宣言しないといけないの?

rustならtrait使ってT: A +Bでいけるのに。

とあって,それに対して

type A interface {
    DoSomething()
}

type B interface {
    DoAnotherthing()
}

func Do(v interface {A; B}) {
    v.DoSomething()
    v.DoAnotherthing()
}

てな感じに書けるよ,という話。もっとも,上の Do() 関数を gofmt にかけると

func Do(v interface {
    A
    B
}) {
    v.DoSomething()
    v.DoAnotherthing()
}

と整形されてしまうけど(笑)

実はこれ「抽象」と「具象」の間に 継承関係はない という Go のとても重要な機能なの。なので,上の Do() 関数のように(仮引数 v の実体が何であるかに関係なく)欲しい振る舞いを示す interface 型を即席で作って 制約を課す ことができる。

たとえば errors 標準パッケージに errors.Unwrap() 関数があるが,これは以下のように実装されている。

errors/wrap.go
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

わざわざ

type Unwrapper interface {
    Unwrap() error
}

みたいな interface 型をあらかじめ宣言しなくても,これで必要十分な機能を提供できる。同様に errors.Is() 関数も

errors/wrap.go
// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
//    func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library.
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

と書かれている。私はこれを見て目から鱗が落ちた。

私もそうだったが, C++ や Java や Rust のような公称型の部分型付け(nominal subtyping)に慣れていると何となく「抽象型を宣言しなくちゃ」と思ってしまうが, Go の場合は抽象型をあらかじめ宣言する必要は微塵もない[1]。むしろ,最初に interface 型を乱発するのは(抽象型に具象型を合わせようという強制力が働くため)開発プロセスの妨げになることさえある。

抽象型で具象型を「囲う」のではなく,必要に応じて最小限の範囲で「接続する」イメージで考えるのがいいのではないだろうか。具象から抽象へ思考(指向)するのが Go 流だと思う。

Go の言語上のメリットのひとつは「継承」という軛(くびき)から自由である,という点だろう。これを実感できるようになれば C++ や Java 上がりのプログラマでももっと自由に Go のコードを書けると思う。

脚注
  1. Go のような型付けシステムを「構造型の部分型付け(structural subtyping)」と呼ぶそうな。 ↩︎

GitHubで編集を提案

Discussion