Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

PythonやJavaScriptなどが隆盛していますが、やっぱり速度重視するとネイティブ吐ける言語になります。
ポストC言語としてGolangやRustなどもあるようですが、どうにも取っ付きににくい。(Rustは触った事ないですけど)
Nimというのが良いらしいとQiitaで記事を見て、興味を持っていたものの、ちゃんと学べていなかったので、その備忘録を兼ねて。
なお、Nimのバージョンは以下の通り。(まだver1.0にもなってない、ある意味成長中のプログラミング言語)

$ nim -v # =>0.19.0

要約

  • カプセル化・オブジェクト生成
    • Nimにはクラスという概念なし。C言語の構造体に近い。
    • オブジェクトは(パフォーマンスにこだわらなければ)参照型で定義すればよさそう。
    • コンストラクタはないので、自前でファクトリープロシージャを用意が必要。
  • メソッド
    • プロシージャの第一引数の型に糖衣構文でバインドされる。Nimの特徴の最たるもの。
    • result変数(戻り値のデフォルト変数)あり
    • オーバーロードあり
  • 可視性の制御
    • モジュール外に公開するか否かで、*を後ろにつける。
  • 総じて
    • Pythonっぽいと言われるが、蛇の皮をかぶったC言語というイメージ。

Nimのオブジェクト指向

  • Nimのオブジェクト指向は最小限(と、公式ドキュメントにも記載)
  • ただ、大体の事はできるので、他の言語(PythonとJava)と比較しながら仕様を確認する。
  • よく「Pythonの様に簡単な記述で・・・」と紹介される事が多いようだが、それはあくまで見た目だけで、そこはかとなくC言語臭がするのがNimだと思う。

カプセル化とオブジェクト生成

オブジェクト指向の本質とはなんぞや?と問われれば、十人十色の回答があるが、自分は カプセル化ではなかろうかと思う。従来(オブジェクト指向以前のプログラミング言語)の多くは、データ構造と処理が分離されていたが、オブジェクト指向以降のプログラミング言語はデータ構造とそれを操作する処理が関連付けられ、外部に公開する必要のないデータや処理は隠蔽された。

で、Nimに関していうとこのカプセル化についてはかなり端折っているし、そもそもクラスという概念がない

じゃあ、オブジェクト指向プログラミングができないのかというと、そういう訳ではなく、必要かつ最小限の事はできる模様。公式ドキュメントにもOOPは最小限!と記述があるが、まぁC言語だって言語仕様としてはクラスはないけれど、オブジェクト指向でプログラミングはできるし。

で、Nimでオブジェクトを生成するにはどうするか?複数のやり方があります。

  1. 値型のオブジェクト利用する
  2. 参照型のオブジェクトを利用する
  3. ポインタのオブジェクトを利用する

値型を用いたオブジェクト定義と生成

オブジェクトが参照型のJavaやPython、JavaScript、Rubyなどと違い、値型のオブジェクトの代入はコピーされる。また、メモリのスタック領域に格納されるので、関数内でオブジェクトを生成してreturnしても、呼び出し元にはコピーが戻る。C++と同じですね。(C++には明るくないけど)

type Person = object
  name : string

var p1 : Person = Person(name:"Bob")

var p2 = p1 # 代入はコピーされる
p2.name = "Julia"

#p1はBobのまま
echo p1.name # => "Bob"
echo p2.name # => "Julia"

参照型を用いたオブジェクト定義と生成

JavaやPythonなどガベージコレクト(以下GC)を持つ言語の多くはこのタイプなので、一番馴染みがあります。
オブジェクトをコピーしたいことは稀なので(メモリの無駄なので)、基本的にはこの参照型でオブジェクトを生成するのがよいと思われます。オブジェクト生成はnewで行い、ヒープ領域に格納されます。(newはキーワードではなく、実は単なるプロシージャ呼出し)

type Person = ref object
  name : string

var p1:Person = new Person
p1.name = "Bob"

var p2 = p1 # 参照のコピーになっているので、実体は同じものを指している。
p2.name = "Julia"

echo p1.name # =>"Julia"
echo p2.name # =>"Julia"

ただちょっと確信がないのが、newを用いずにオブジェクト生成した場合は、GCの対象にならないと思われる事。(ソースを見る限りでは、newテンプレートの中でGCに関する処理をしているので、newしないとGCの対象にはならないっぽい)
newを用いずにオブジェクト生成しても、暗黙的にnewが呼び出されるためGCの対象になります。(参考:Nimマニュアル#Object Construction

var p1 : Person = new Person # ドキュメントによるとGCの対象になる
p2.name = "Bob"

var p2 : Person = Person(name:"Bob") # これはGCの対象にならない? → GCの対象になる。暗黙的にnewしている。

ポインタを用いたオブジェクト定義と生成

最後にポインタを用いたオブジェクト生成ですが、これはC言語のライブラリなどを使ったりする時に利用するもので、特殊な場合をのぞいて利用する必要はなさそうです。ちなみにGCの対象にならないので、自分でメモリ管理する必要ありです。
やっている事はほとんどC言語。

type Person = ptr object
  name : string

# allocでメモリを確保
var p : Person = cast[Person](alloc(sizeof(Person)))
p.name ="Bob"

echo p.name

GCunref p.name # GCにもう不要と教えてあげる
dealloc p # メモリ解放

コンストラクタ

Nimにはコンストラクタはなく、フィールドは初期値(0nil)で初期化されている。高度な初期化をしたければ、初期化用プロシージャを作成しないといけない。
これは正直、うーん・・・という感じ。人によってはnewPersonという名前にしたりcreatePersonという名前にしたり・・・と、統一されなくなりそう。ざっと標準ライブラリをみた限りでもcreateXXXXnewXXXXがあったので、標準ライブラリ内でも統一されていない様に見受けられる。
また、Javaのようにデフォルトコンストラクタをprivateにして、ファクトリーメソッドからのインスタンス生成しか許容しないようなパターンも実現できないと思われます。

メソッド(ではないが)の定義

第一引数のオブジェクトに、バインドされているかの様な糖衣構文

Nimではメソッドではなくプロシージャと言う。他のClassを持つ言語と異なり、プロシージャはオブジェクトにバインドされていない。プロシージャの第一引数をオブジェクトにし、procedure(obj)の糖衣構文であるobj.procedure()でバインドされているかの様に見せている。

type Person = ref object
  name : string

# Personを第一引数にとるプロシージャ
proc hello(p:Person):string =
  "Hello,I'm " & p.name #最後の式が暗黙的にreturnされる

var p:Person = Person(name:"Bob")

#以下、全部同じ意味
echo p.hello
echo p.hello()
echo hello(p)
echo hello p

これによって、他の多くの静的型付言語と異なり、既にある型へのメソッド追加ができるようになる。たとえプリミティブな型であっても以下のようにメソッドを追加する事が可能。(動的型付け言語でもプリミティブ型へのメソッド追加はできないケースが多いと思う。)

# 第一引数がintのプロシージャ
proc times(n:int,action:proc)=
  for i in 1..n:
    action()

# プリミティブなint型にtimesメソッドが追加されたかの様な挙動
3.times(proc()=echo "Hello") # "Hello"が3回出力される

また、面白いのが以下のように引数の型をorで繋ぐことができる。実際は2つのhelloプロシージャに展開されているもと思われる。

type Person = ref object
  name:string
type Animal = ref object
  name:string

proc hello(x:Person or Animal):string=
  "hello,I'm",x.name

let p = Person(name:"Bob")
let a = Animal(name:"Pochi")

echo p.hello
echo a.hello

result変数

result変数は特殊な変数名で、プロシージャは`return文がなければ、result変数の値を返す。
result変数もなければ、最後に評価された式の結果を返す。すなわち、以下の3つのプロシージャはいずれも同じ結果になる。

proc a():string=
  return "hello"
proc b():string=
  result = "hello"
proc c():string=
  "hello"

オーバーロード

静的型付言語なので、当然オーバーロードあり。

proc a(n:int):string = "Arg is Integer"
proc a(f:float):string = "Arg is Float"
echo a(10) #=> "Arg is Integer"
echo a(10.0) #=> "Arg is Float"

可視性の制御

*付けてモジュール外へ公開する

Nimの可視性の制御は、Javaでいうパッケージプライベートのみ。細かい制御はできない。
モジュール外に公開するものは*を後ろにつける。(なんとなくポインタを彷彿させちょっと嫌...)

sub.nim
# 型を公開
type Person* = ref object of RootObj =
  name*:string # 公開フィールド
  age:int      # 非公開フィールド

# コンストラクタ代わり
proc newPerson*(name:string,age:int):Person =
  var p = new Person
  p.name = name
  p.age = age # 同一モジュール内はアクセス可能
  return p

# 公開プロシージャ
proc hello*(p:Person) = 
  echo "I'm ',p.name
  echo p.age,"years old"
main.nim
import sub
let p = newPerson("Bob",30)
p.hello
echo p.name
echo p.age # =>非公開フィールドなのでコンパイルエラー!

継承

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away