はじめに
先日、AtCoder Heuristic First-step Vol.1 - AtCoder が開かれたこともあってか、ヒューリスティックに関する話題を良く見かけます
AHCの参加人数が増えるのはとても良い事なので嬉しい流れだと思っています
少しの懸念としては、Python等の実行速度が遅い言語で参加して、それを理由に諦めてしまう人がいるかもしれない。ということです
私自身、取り組み始めてから約1年はPythonでAHCに参加しており、実行速度を理由に諦めかける時もありました(Pythonだけでも強い人は何人もいます)
そんな人たちに選択肢の一つとしてNim言語を紹介することで、あわよくば使い手になって欲しいなと思い記事を書くことにしました
良ければ読んでいって下さい
良いと思う点
実行速度が早い
Pythonと比べると。という意味では速度が一番のメリットです
Nimは、C++にトランスコンパイルしてから実行されるため高速に動作します
実装方法によってかなり前後しますが、C++はNimの2倍程度速く、NimはPythonの5倍程度速いイメージです
いい感じのベンチマーク結果を見つけられなかったため、ABCの提出時間等で比較してみてください
大体上記のような感じになっていると思います
文法がかなりPythonに似ている
文法が似ている≒学習コストが低いと言えます
Nim言語は、型が付いただけのPythonと呼んでも差し仕えない程度には似ています
ABC402 B - Restaurant Queue で比べてみます
from collections import deque
Q = int(input())
q = deque()
for _ in range(Q):
buf = [int(i) for i in input().split()]
if buf[0]==1:
x = buf[1]
q.append(x)
else:
print(q.popleft())
Nim
import std/deques
var Q = input(int)
var q = initDeque[int]()
for _ in 0..<Q:
let f = input(int)
if f==1:
let X = input(int)
q.addLast(X)
else:
echo q.popFirst()
dequeを使った実装ですが、型の指定以外はほぼ同じ記述です
インデントでブロックを区切る、セミコロン不要、などは親和性が高いのかなと思います
ちなみに、nimのdequeはランダムアクセスがO(1)です
enumerateを書かなくてよい
Pythonで配列Aの中身とインデックス番号を同時にループで回すには以下のように書くと思います
A = [1, 3, 5, 7]
for i, a in enumerate(A):
print(i, a)
Nimでは以下のように書けます
var A = @[1, 3, 5, 7]
for i, a in A:
echo (i, a)
i, をfor文の最初に付けるだけでインデックス番号も一緒に取得できます
もちろん、for a in A: のように書くだけにしてループを回すこともできます
block文で多重ループを一気に抜けられる
Pythonで多重ループを抜けたい場合、フラグなどを用いて処理するかと思います
もしくは、for-elseを用いてもループ内のbreakを検知できます
flg = False
for i in range(10):
for j in range(10):
for k in range(10):
if i+j+k>=10:
flg = True
break
if flg: break
if flg: break
Nimには、block文があり、ラベルを指定してbreakすることができます
block loop:
for i in 0..<10:
for j in 0..<10:
for k in 0..<10:
if i+j+k>=10:
break loop
インデントが一段深くなってしまいますが、シンプルに記述できるのは嬉しい人も多いのではないでしょうか
コンパイル時の前処理が簡単
Pythonでコンパイル時間を利用した前処理を書くこともできるようですが、あまり実用的でない(Cythonのみ?)と認識しています
Nimでは、コンパイル時に計算できる処理であれば、constを付けて宣言するだけでコンパイル時に前計算を済ませておくことが出来ます
いくつか自分が使用している例を置いておきます
エラトステネスの篩で素数を列挙しておく(ACL-Nimを利用)
import atcoder/extra/math/eratosthenes
const primes = initEratosthenes(10**6).prime
焼きなまし遷移用の乱数準備
var rng {.compileTime.} = initRand(0x1337DEADBEEF)
const logList = newSeqWith(0x10000, ln(rng.rand(1.0)))
他にも、固定マス数のグリッドグラフの隣接リストやZobristHash用の配列を作ったりしています
糖衣構文(シンタックスシュガー)が面白い
聞き慣れない言葉だと思いますが(自分も調べて今知りました)、同じ意味の処理を複数の書き方(より簡単な)が出来るというものです
proc add1(a: int): int=
return a + 1
let x = 10
# 引数が複数あっても使える書き方
echo add1(x)
echo x.add1()
# 引数が1つの時のみ使える書き方
echo add1 x
echo x.add1
引数が1つの時のみ、括弧()を省略することができますが、これを使うかどうかは人によって好みが分かれるようです(自分は好きです)
ところで、Nimにはclassという概念がありません
その代わり、関数宣言の第一引数のオブジェクトにバインドしているかのように記述することが出来ます(上記のドット記法)
Pythonでのクラス定義とメソッド
class Person():
def __init__(self, height, weight):
self.height = height
self.weight = weight
def get_bmi(self):
return 10000 * self.weight / self.height / self.height
p = Person(170, 65)
print(p.get_bmi())
Nimで同じような処理を書く場合
type Person = object
height: int
weight: int
proc initPerson(height, weight: int): Person=
result.height = height
result.weight = weight
proc getBMI(self:var Person): float=
return (10000 * self.weight).float / self.height.float / self.height.float
var p = initPerson(170, 65)
echo p.getBMI()
getBMI() の宣言を見ると、第一引数にPersonオブジェクトを取っています
このような場合、Personオブジェクトのクラスメソッドかのように、p.getBMI() と関数を呼び出して使用することが出来ます
メソッドチェーンでスマートに書ける
第一引数の型にドット記法で関数を呼び出す事を利用すると以下のような書き方も出来ます
let dx = 10
let dy = 5
echo (dx**2 + dy**2).float.sqrt.int
上記は整数のユークリッド距離を求めるコードですが、計算結果を変換していく手続きを直感的に書き連ねることが出来ています
pythonでは、関数を必ず前に書くためこのようには書けません
また、自分で宣言したオブジェクト以外にも、既存の型に対しても同じように関数を宣言して使用することも可能です(これが好き)
例えば、以下のようにint型に対して3乗を求める関数を用意することが出来ます。便利です
proc pow3(n: int): int=
return n**3
let num = 10
echo num.pow3()
演算子を自分で定義し直せる
Nimでは演算子も関数の一種です(NimWorld | 関数)
演算子の定義をし直すことで、自分の好みの記述方法に変えることが出来ます
例えば、累乗の演算は `^` が定義されていますが、Pythonでは xor を表すため紛らわしいです
Pythonのように書きたければ、以下のように定義し直すことで使いやすくなります
proc `**`(x: int, y: int): int = x ^ y # ** を累乗の演算子に再定義
proc `^`(x: bool, y: bool): bool = x xor y # ^ をxorの演算子に再定義
このように、テンプレートを整備することで自分にとって一番分かりやすい環境を整えることが出来ます
イマイチだと思う点
インデントにTabが使えない
Nimは、スペース2つをインデントとすることがデフォルトになっています
pythonから移行した時にここがストレスでしたが、今は全ての言語でスペース4つのインデントに統一したため自分は全く問題に感じてはいません
謎のバグを踏むことがある
他の言語と比べると、歴史も浅くユーザー数も多いとは言えないため言語自体にバグがあることもあります
また、検索してもあまりヒットせず解決に時間がかかる場合もあります
tuple型の要素アクセスに変数を使えないことを知らず、調べても上手く見つからず数時間溶かしたこともあります
ChatGPTのコード生成の精度が悪い
最新のo3などでは試していませんが、少なくともo1のモデルが正しいNimコードを出力することは難しく、コードの修正だけで2,3回のやり取りが発生しています
自分が生成AIを使用する際は、pythonで出力してもらったものをNimに書き換えて使っています
言語の使用者や、ネット上の記事が増えるとそのうち改善されるのかなと思っていますが、正直生成AIの恩恵を受け切れていない感はあります
良いプロンプトを知っていれば是非教えてください
おわりに
いかがだったでしょうか
Nim言語の便利さ、面白さが少しでも伝わっていれば嬉しいです
いくつかデメリットも書きましたが、それらを補って余りあるメリットもある言語だと思っています
持ち替え検討しようかなという方がいれば、出来る限りサポートしますので気軽に連絡下さい!