M.Hiroi's Home Page

お気楽 Lua プログラミング超入門

多重継承と Mix-in


Copyright (C) 2011-2019 Makoto Hiroi
All rights reserved.

はじめに

継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。これに対し多重継承は複数のクラスを継承することができます。Lua の場合、メタテーブルの __index で指定できるテーブルはひとつだけなので、この方法では単一継承になります。

●単一継承と多重継承

単一継承の場合、クラスの階層は図 1 のような木構造 [*1] で表すことができます。

            A
          /|\
        /  |  \
      B    C    D
    /  \
  /      \
E          F

図 1 : 単一継承におけるクラスの階層

継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D と 3 つ、B のサブクラスは E, F と 2 つあります。図 1 では、クラス A のスーパークラスはありませんが、ほかのクラスではただひとつのスーパークラスを持っています。プログラミング言語では、Smalltalk, Java, Ruby が単一継承です。

これに対し多重継承は、複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、図 2 のようなグラフ [*2] で表すことができます。

              A
            /  \
          /      \
        B          C
      /  \      /  \
    /      \  /      \
  D          E          F

図 2 : 多重継承におけるクラスの階層

クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができます。C++, Common Lisp Object System (CLOS), Python は多重継承をサポートしています。

-- note --------
[*1] 木 (tree) は階層的な関係を表すためのデータ構造です。身近な例ではディレクトリ (フォルダ) の階層構造が木にあたります。
[*2] グラフは木をより一般化したデータ構造です。数学のグラフ理論では、いくつかの点とそれを結ぶ線でできた図形を「グラフ」といいます。

●多重継承の問題点

多重継承は異なる性質や機能を持つ複数のクラスを継承することができるので、とても強力な機能です。ところが問題点もあるのです。たとえば、クラス Foo にはメソッド method_a があり、クラス Bar にはメソッド method_b があるとしましょう。この 2 つのメソッドはまったく異なる働きをします。ここで、メソッド method_a はインスタンス変数 x を使っていて、method_b も x を使っていると、多重継承で問題が発生します。

クラス Foo と Bar を多重継承してクラス Baz を作成した場合、クラス Baz のインスタンスには x がひとつしかありません。メソッド method_a と method_b はひとつしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありません。また、Foo と Bar に同じ名前のメソッドが存在することもあります。このように、多重継承では名前の衝突が発生する場合があるのです。

それから、多重継承にはもうひとつ問題点があります。それはクラスの階層構造が複雑になることです。単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単にわかりますが、グラフになると優先順位を理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要です。

●Mix-in

これらの問題を回避するため、インスタンス変数 (属性) を継承するスーパークラスはひとつだけに限定して、あとのスーパークラスはメソッド (実装) だけを継承するという方法があります。この方法を Mix-in といいます。具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。ひとつのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。

なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。ちなみに、この Mix-in という方法を言語仕様に取り込んだのが Ruby です。

Mix-in を図に示すと次のようになります。

                A
              /
            B
 Mixin A  /  \    Mixin B
    \  /      \  /
      C          D

      図 3 : Mix-in

クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。

多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。

●Enumerable

Mix-in はクラスのないプロトタイプベースのオブジェクト指向でも簡単に実現することができます。Mix-in は簡単にいえばクラスにメソッドを追加することです。Lua の場合、クラスを表すオブジェクト (テーブル) にメソッドを追加することで Mix-in を実現することができます。

それでは Mix-in の例題として、Mix-in 用のオブジェクト Enumerable を作ってみましょう。Enumerable は配列や連結リストなどのような複数のデータを格納するオブジェクトに高階関数 (メソッド) を Mix-in します。これは Ruby のモジュール (Mix-in 用のクラス) Enumerable を参考にしました。追加するメソッドを表 1 に示します。

表 1 : Enumerable のメソッド
名前機能
obj:member(func)func が真となる要素を返す
obj:position(func)func が真となる要素の位置を返す
obj:count(func)func が真となる要素の個数を返す
obj:map(func)要素に func を適用した結果をリストに格納して返す
obj:filter(func)func が真となる要素をリストに格納して返す
obj:fold_left(func, init)すべての要素を func を用いて結合した結果を返す

プログラムは次のようになります。

リスト 3 : Enumerable 

-- クラス定義
Enumerable = {}

-- 探索
function Enumerable:member(func)
  for v in self:each() do
    if func(v) then
      return v
    end
  end
  return false
end

-- 位置を返す
function Enumerable:position(func)
  local n = 1
  for v in self:each() do
    if func(v) then
      return n
    end
    n = n + 1
  end
  return -1
end

-- 条件を満たす要素をカウントする
function Enumerable:count(func)
  local n = 0
  for v in self:each() do
    if func(v) then
      n = n + 1
    end
  end
  return n
end

-- マッピング
function Enumerable:map(func)
  local a = {}
  for x in self:each() do
    table.insert(a, func(x))
  end
  return a
end

-- フィルター
function Enumerable:filter(pred)
  local a = {}
  for x in self:each() do
    if pred(x) then
      table.insert(a, x)
    end
  end
  return a
end

-- 畳み込み
function Enumerable:fold_left(func, init)
  local a = init
  for x in self:each() do
    a = func(a, x)
  end
  return a
end

オブジェクトの要素はメソッド each で取り出します。each は Mix-in するオブジェクトで定義することとします。つまり、each を定義さえすれば、どんなオブジェクトにも Enumberable を Mix-in することができるわけです。あとは、Enumerable のメソッドで each を呼び出して結果を返すだけです。

たとえば member の場合、for v in self:each() do ... end で要素を取り出して変数 v にセットします。func(v) が真を返す場合は v を return で返します。map も同様に for ループで要素を取り出します。その中で func(x) を実行し、その結果を局所変数 a の配列に追加します。最後に return で a を返します。他のメソッドも同じようにプログラムすることができます。

最後にメソッドを Mix-in する関数を作ります。

リスト 4 : Mix-in

-- src に dst を MIX-IN
function mix_in(dst, src)
  for k, v in pairs(src) do
    dst[k] = v
  end
end

関数 mix_in はオブジェクト src にあるフィールドをオブジェクト dst にコピーします。フィールドの取得は for 文と関数 pairs を使うと簡単です。pairs はキー k と値 v を返すので、dst[k] = v とすれば、src にあるフィールドを取り出して dst に追加することができます。

たとえば、List を継承した EnumList に Enumerable を Mix-in する場合は次のようにします。

リスト 5 : List に Enumerable を Mix-in する

EnumList = {}

-- List を継承
setmetatable(EnumList, {__index = List})

-- Enumerable を MIX-IN
mix_in(EnumList, Enumerable)

-- コンストラクタ
function EnumList.new(...)
  local obj = List.new(...)
  return setmetatable(obj, {__index = EnumList})
end

これで、EnumList のオブジェクトから Enumerable のメソッドを呼び出すことができます。簡単な実行例を示しましょう。

> a = EnumList.new(1,2,3,4,5,6,7,8)
> a:each(print)
1
2
3
4
5
6
7
8
> a:member(function(x) return x == 5 end)
5
> a:member(function(x) return x % 2 == 0 end)
2
> a:position(function(x) return x % 2 == 0 end)
2
> a:position(function(x) return x % 4 == 0 end)
4
> a:count(function(x) return x % 2 == 0 end)
4
> a:count(function(x) return x % 3 == 0 end)
2
> table.concat(a:map(function(x) return x * x end), ',')
1,4,9,16,25,36,49,64
> table.concat(a:filter(function(x) return x % 2 == 0 end), ',')
2,4,6,8
> a:fold_left(function(x, y) return x + y end, 0)
36

正常に動作していますね。また、配列にも Enumerable を Mix-in することができます。

リスト 6 : 1次元配列に Enumerable を Mix-in

EnumVector = {}

-- Enumerable を Mix-in
mix_in(EnumVector, Enumerable)

-- コンストラクタ
function EnumVector.new(...)
  local obj = {...}
  return setmetatable(obj, {__index = EnumVector})
end

-- each メソッド
function EnumVector:each()
  return coroutine.wrap(
    function()
      for i = 1, #self do
        coroutine.yield(self[i])
      end
    end)
end

簡単な実行例を示します

> a = EnumVector.new(10,20,30,40,50)
> a
{10,20,30,40,50}
> a[1]
10
> table.concat(a:map(function(x) return x * x end), ',')
100,400,900,1600,2500
> a:member(function(x) return x == 30 end)
30
> a:member(function(x) return x ~= 30 end)
10
> a:member(function(x) return x > 30 end)
40
> a:fold_left(function(x, y) return x + y end, 0)
150

このように、複数のクラスで共通の操作 (メソッド) を定義したい場合、Mix-in はとても役に立ちます。


初版 2011 年 5 月 7 日
改訂 2019 年 12 月 28 日