はじめに
Nimって言語のver 1.0.0
がりりーすされたみたいなので触ってみたお話です.
Nimって調べるとPython風な書き方ができる高速な言語って出てきます.
この記事ではNimの導入とnimpyっていうPythonからNimを使うやつのお話をします.(NimからPythonを呼ぶこともできるみたいですがここでは触れません)
Nimの基本的な導入記事としては
が個人的におすすめです.
Nimのインストール方法
先ず,なにはともあれNimをインストールしなければなりません.公式サイトに従ってインストールしましょう.
僕がUbuntu 18.04でインストールした方法を書いておきます.apt
でもインストールできるのですが,バージョンが0.19
とかなので,公式からダンロードしました.
$ curl https://nim-lang.org/choosenim/init.sh -sSf | sh
これで大丈夫です.このコマンドを実行すると,ホームディレクトリに~/.nimble/
が作成されて,~/nimble/bin/
にインストールされます.なので,~/.nimble/
をパスに追加しましょう.
nimpyのインストールとすごさ
$ nimble install nimpy
これで大丈夫.Nimのライブラリのインストールはpipみたいに使えて楽で
僕は普段Pythonを使っているので,Pythonから使えるような言語でないとあんまり勉強する気が起きませんし,勉強したところで結局使いません.C++のBoost Pythonを使っていたこともあるのですが,やはりインストールが多少手間で,Pythonを普段書いてるような矮小な私にとってはC++を書くのも結構大変で骨が折れます.
さて,nimpyですがこれはすごいです.これを使うことでPythonからNimの関数を呼び出せるようになります!!!
Boost Pythonと比べての個人的に感じるメリットとして
- 導入,コンパイルが楽
- Pythonの型(listやtuple)をNimの型(seq)に勝手に直してくれる (これはBoost Pythonではできない)
- Nimのプロシージャに
{.exportpy.}
プラグマをつけるだけでPythonに公開できる. - Nimの書き方はPythonと似ているので並列して書いても頭がおかしくならない
などが挙げられます.
Boost Pythonも好きですが,僕が書くことはもうないでしょう.
LET'S USE NIMPY
Hello nimpy
まず簡単に,以下のようなnimodule.nim
を書きます
import nimpy # nimpy をインポート
proc hello(): void {.exportpy.} =
# {.exportpy.}の書かれたプロシージャが公開される
echo "HELLO NIMPY!!!"
以下のコマンドでコンパイルします.
$ nim c --tlsEmulation:off --app:lib --out:nimodule.so nimodule.nim
そしたら,カレントディレクトリにnimodule.so
ができます.
ここでpython3
を起動し,
$ python3
>>> import nimodule
>>> nimodule.hello()
HELLO NIMPY!!!
>>>
なんと簡単なのでしょうか!!!
CやC++でモジュールを作る場合こんな簡単にはできないです.
+, -, *, / and mod
つぎに,2つの数の四則演算とあまりを表示して,なんとなく1
を返す関数をつくってみます.
import nimpy, strformat
proc echoAxB(a, b: int): int {.exportpy.} =
echo fmt"a+b = {a + b}" #pythonのf-strings 要 strformat
echo fmt"a-b = {a - b}"
echo fmt"a*b = {a * b}"
echo fmt"a/b = {a / b}"
echo fmt"a%b = {a mod b}" # %ではなくmod, 結構罠
result = 1 # result は特別な変数で,この値が暗黙的にreturnされます.
同様にコンパイルし,
$ python3
>>> import nimodule
>>> a = nimodule.echoAxB(3,2)
a+b = 5
a-b = 1
a*b = 6
a/b = 1.5
a%b = 1
>>> print(a)
1
>>> b = nimodule.echoAxB(2.0, 1.5)
a+b = 3
a-b = 1
a*b = 2
a/b = 2.0
a%b = 0
はい,こんな感じです.ここで,Python側からfloat型を渡すと,勝手にint型に変換されます.また,float型を引数にもつ関数にint型を渡す場合も変換されます.
list, tuple and dict
ここではpythonの配列→Nimの配列の対応を示します.
list, tuple → seq
例えば,配列の正の要素の総和をもとめる関数をNimでかくと
import sequtils
proc sumPositiveElements[T](x: seq[T]): T =
x.filter(proc (x:T): bool = x>0).foldl(a+b)
# 最後に評価される式が暗黙的に返り値になります
# filterはPythonと同様,引数はLambda関数
# foldlはPythonのreduce (foldrもある)
if isMainModule: # pythonのif __name__ == "__main__":
echo sumPositiveElements(@[-2, -4, 5, 3]) # => 8
となります.ここで[T]
はジェネリックスと呼ばれるもので,C++のテンプレートのようなもので,型をパラメータ化できます.しかしこれに{.exportpy.}
プラグマをつけてもコンパイルできません.int
かfloat
を明示的に書かなくてはいけません.int
として実装すると,
>>> nimodule.sumPositiveElements([-5, -3, -7, 1, 3])
4
>>> nimodule.sumPositiveElements((-5, -3, -7, 1, 3))
4
となります.ListとTupleは動的配列seqに変換されます.
list, tuple → tuple
さて,先程のような場合,配列の要素が全て同じ型でなければなりません.異なった型の要素を持つ配列の場合は以下のようにseq
でなく,tuple
に変換するすることで対応できます.
proc echoTuple(t: tuple[name:string, age:int]): void {.exportpy.} =
echo fmt"Name: {t.name}, Age: {t.age}"
Nim側では,引数をtuple
とし,それぞれの型を明示することでPythonの配列をうけとれます.Nimのtupleはフィールド名を持ち,簡単な構造体のように扱えます.とても便利!!さて,Python側からは以下のように呼び出せます.
>>> import nimodule
>>> nimodule.echoTuple(("Taro", 32)) # list型["Taro", 32]でもおk
Name: Taro, Age: 32
dict → dict
Pythonの基本的な型として辞書型dict
がありますが,これはNimのtable
型に変換できます.以下は配列を要素に持つ辞書から,最大の平均の配列のキーとその平均値を返す関数です.Nimで複数の返り値を設定する場合もtuple
を使うことで実現できます.
import tables
proc maxMeanColumn(d: Table[string, seq[int]]): tuple[key: string, mean: float] {.exportpy.} =
result = (key: "nil", mean: -high(float)) # high(float)でfloatの最大値
for key, values in d: # pythonでのd.items()
let mean = values.foldl(a+b).float/values.len.float # 型キャスト
if mean > result.mean:
result = (key, mean) # フィールド名は省略可能
Python側から適当な辞書を渡します.
>>> import nimodule
>>> d = {"a": list(range(10)),
... "b": [10, 20, 0],
... "c": [30, 5]}
>>> nimodule.maxMeanColumn(d)
('c', 17.5)
はい,便利ですね.
NimとPythonのパフォーマンスの差なども検証してみたいです.(参考)
function as an argument and PyObject
さて,引数として関数を渡したい場合があります(高階関数).
$f: \{0,...,10\} \to \{0,1\} $な関数を考えてみます.$f(0)=f(x)$となる$x (x>0)$をすべて探すプログラムは以下のようにかけます.
proc enumerateAll(f: proc(x: int): int): seq[int] {.exportpy.} =
# このように引数を書いたら関数を渡せる
let y = f(0)
var solutions: seq[int] = @[] # ここで:seq[int]の明記が必要
for i in 1..10: # Pythonのrange(1, 11), 10も含まれることに注意
if y == f(i):
solutions.add(i) # seqではappendではなくadd
return solutions # returnもちゃんとあります
$f$をPython側で以下のように実装し,解を求めます.
>>> def f(x):
... if x in [0, 3, 7, 8, 10]:
... return 1
... else:
... return 0
>>> nimodule.enumerateAll(f)
[3, 7, 8, 10]
今のままでは,返り値がint
でないといけません.もう少し一般的に引数のところをf: proc(x: int): PyObject
とすれば,比較可能なPythonオブジェクトを返す関数を渡すことができます.
nimpyの欠点
個人的に思うnimpyの最大の欠点はnimpyでグーグル検索するとnumpyが引っかかること."nimpy"のようにクォートつけないとまともに記事が見つからない.
あと,Nimのtype
(構造体)を返り値にできないらしい.
おわりに
Nimはいいぞ
ここで書いたプログラムに実用的なものはないと思いますが,調べたことをまとめてみました.
nimpyが公式サポートになったらとさらにNimユーザが増えると思いますが,どうなのでしょうか...(nimpyの作者はnimのcontributorなので,期待できそう?)