LoginSignup
22
12

More than 5 years have passed since last update.

Nimのオブジェクト指向の整理その2

Posted at

はじめに

前回、から随分時間がたってしまったが、積み残していた「継承」などを中心に。Javaと比較しつつ。
nimのバージョンは以下の通り。

$nim -v #=>0.19.0

まとめ

  • 継承は多重継承できない
  • 親メソッド呼びは、ちょっと独特
  • インターフェイス型がない

JavaとNimの比較

比較項目 Java Nim
継承 class Child extends Base{ type Child = ref object of Base
キャスト(型変換) Base b = (Base) child; var b:Base = Base(child)
動的ディスパッチ・オーバライド @Override
public void hoge(){...}
method hoge(self:Child)
親メソッド呼び super.hoge(); procCall Base(self).hoge()
インターフェイス型 interface IHogeable{
void hoge()
}
ない・・・と思う。
クロージャで、っぽい事はできる

Nimにおける継承

NimはJavaなどに比べて、「継承」機能はあまり充実していない。公式でも「”is a”より”has a”の方がいい場合が多いよね」みたいなスタンスの模様。ただ、必要十分な機能は備えているかなと思う。

基本

Nimでの継承の基本は

  • 親クラスはRootObjを継承しておく。(でないとサブクラスを作れない1
  • 子クラスはofで継承元を指定する
type
  Person = object of RootObj #Javaの`Object`相当である、RootObjを継承しておく
    name:string
    age:int
proc hello(p:Person)=
  echo fmt"I am {p.name}. I am Person!"
proc howOld(p:Person)=
  echo fmt"I am {p.age} years old."

type
  Student = object of Person #Personを継承
    grade:int
proc hello(s:Student)=
  echo fmt" am {s.name}. I am {$s.grade} grade Student!"

let p = Person(name:"Bob",age:20)
let s = Student(name:"Julia",age:18,grade:4)

p.hello #=> 親のhelloプロシージャ 
s.hello #=> 子のhelloプロシージャ
s.howOld #=> 子に定義はない。親のhowOldプロシージャが呼ばれる

ここまでは、いたって想定通りの挙動。

キャスト(型変換)

型変換は型名(変数)で行うが、以下の2行目はエラーになる。

Person(s).hello #=>親のプロシージャがちゃんとよべる
var p2:Person = Person(s) #=> 実行時エラー!!

これは、PersonStudent値型のため代入時のコピーでエラーになっている模様。型定義で参照型にしてやればよい。(というような事があるので、原則的にobjectタイプは参照型で定義するのがよいと思われる。)

type
  Person = ref object of RootObj
    name:string
    age:int
  Student = ref object of Person
    grade:int

動的ディスパッチ(メソッドのオーバーライド)

プロシージャのオーバーライドは、C++のvirtualみたいに(C++に明るくないけど)明示的にする必要がある。明示の仕方はprocmethodにすればよい。この時、ベースのメソッドには{.base.}プラグマを付けろとウォーニングが出るのでつけておく。

type
 Person = ref object of RootObj
 Student = ref object of Person

method hello(self:Person){.base.} =
  echo "Person"
method hello(self:Student)=
  echo "Student"

var persons:array[2,Person] = [new Person,new Student]
for p in persons:
  p.hello
#=> Person
#=> Student

では、やっぱり親のメソッドを呼び出したい時はどうするのというと(Javaでいうsuper.hello();みたいなの)、ちょっと冗長だけれどprocCallと型変換で実行できる。

method hello(self:Student)=
  Person(self).hello # ダメ。オーバライドしているのが呼び出される(この例だと無限ループでスタックオーバーフローする)
  procCall Person(self).hello # procCallをつけて呼ぶと、superみたいな挙動する。

インターフェイス型

Javaでいうinterface相当のものはない。Javaのinterfaceの役割は

  • 多態性としてinterface型に変換できる
  • interfaceで定義されたメソッドの実装を強要できる

ことかなと思うけれど、これを実現するのは、ちと難しそう。代わりに以下のようにクロージャを利用すれば、似たような事もできなくはないけれど…冗長だな。何か他にいい案ないものか。

Javaだったらこう書くけれど・・・
interface Soundable{
  public String sound();
}
class Duck implements Soundable{
  public String sound(){
    return "quack";
  }
}
class Dog implements Soundable{
  public String sound(){
    return "bow!!";
  }
}
class Test{
  public static void main(String[] args){
    Duck duck = new Duck();
    Dog  dog  = new Dog();
    Soundable[] animals = {duck, dog};

    for (Soundable animal:animals) System.out.println(animal.sound());

    System.out.println(animals[0] == duck); // => true
  }
}
nimのinterfaceっぽいなにか
type
  Soundable = ref object of RootObj #interface相当の型
    sound:proc ():string
  Duck = ref object of RootObj
  Dog  = ref object of RootObj

proc getSoundable(self:Duck):Soundable= #Soundableを返すメソッド、Javaのimplements相当
  const s = "quack"
  Soundable(sound:proc():string=s) #Duckクラスのsoundの実装相当

proc getSoundable(self:Dog):Soundable= #Soundableを返すメソッド、Javaのimplements相当
  const s = "bow"
  Soundable(sound:proc():string= #Dogクラスのsoundの実装相当
    result.add(s)
    result.add("!!")
  )

let duck = new Duck
let dog  = new Dog
let animals:array[2,Soundable] = [duck.getSoundable,dog.getSoundable]

for animal in animals:
  echo animal.sound() #=>"quack" "bow!!"

echo animals[0] == duck #=>ただし、当然これは型が違うのでコンパイルエラー

書いた後に、nim interfaceでGoogle先生にきいてみると、いくつか有用そうなのがヒットした。「Nimにインターフェイスは必要か?」はNimコミュニティでも議論されている模様。

objectではなくtupleでインターフェイス型をつくるパターン

基本的には上記と同じだが、インターフェイス型相当をref objectではなくtupleにしている。

マクロでやっちゃうパターン

interfaced
やってることは基本的に上記と同じ模様。createInterface(インターフェイス名):でインターフェイスを作り、toインターフェイス名でインターフェイス型に変換するマクロになっている。

import interfaced
createInterface(Soundable):
  proc sound(self:Soundable):string

proc sound(self:Duck) = "quack"
proc sound(self:Dog ) = "bow!"

let duck = new Duck
let dog  = new Dog
let animals = [duck.toSoundable,dog.toSoundable]
for animal in animals:
  echo animal.sound()
Templete? Concept? マクロ?

まだ、TempleteやConceptをちゃんと理解できていないので、要調査。

おわりに

インターフェイスもなく、多重継承も認めていないという点で、ちょっと辛いところはあるかもしれないが、十分にオブジェクト指向な設計ができると思う。
まだまだNimはマイナー言語だけど、Go言語よりはやってて楽しい。2 ただ、問題はエコシステムがまだ確立していない。

でもNim超偉い。もっとがんばれ!超がんばれ!


  1. またはfinalプラグマを付けても継承できない。反対にRootObj継承していなくてもinheritableプラグマ付けると継承できる。 

  2. 個人の感想、別にGoを批判する意図はない。どう考えても現状はGoの方がエコシステムも完成しているし、実用されている。ただ、「こう書きたいな」というのに、Nimは大体応えてくれるので、書いて楽しいのかなと思う。 

22
12
0

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
22
12