Nim言語の良さを伝えたい

はじめに

 先日、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 で比べてみます

Python

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言語の便利さ、面白さが少しでも伝わっていれば嬉しいです

 いくつかデメリットも書きましたが、それらを補って余りあるメリットもある言語だと思っています

 持ち替え検討しようかなという方がいれば、出来る限りサポートしますので気軽に連絡下さい!