Pythonみたいな文法でCにトランスパイルされる言語に入門
レポート締め切りがすぐそばに迫っていて浪人どころか高校卒業すら怪しいオタク,おーみーです.
この gist は 存在しない技術 Advent Calendar 2021 の4日目の記事です.主催者のmomeemtさんから,存在する技術について書くとその技術が消滅するとの公式見解を頂いたので,氏の推し言語であるNimを消していきます.
Nim やる前の印象
「第二プログラミング言語として Rust はオススメしません Nim をやるのです」「イケてないのに人気がある golang vs イケてるのに人気がない Nim」の印象がめちゃくちゃ強いです.私はこれら3言語の中ではRust派で,これらの記事には同意できないところが多い1のですが,大事なブログをはてなブックマークとTwitterで炎上させてまでage(死語)たい,その手の人たちには大規模構造を隔てているとさえ感じさせる,Nimという言語はいったいどんなものなのかということが気になりました.
真面目な話に戻して,言語の内容としては,
- 静的型付け
- インデント構文があってPythonっぽい
- ガベージコレクションする
- Cに変換してからGCC/Clangで機械語にコンパイルする
- Universal Function Call Syntax (UFCS) がある
- Cとの連携がやりやすそう
- momeemtがやっていたので
- マクロが強いらしい
- momeemtが言っていたので
という情報を知っていました.プログラミング言語オタクなのでUFCSが一番興味あります.
環境構築
choosenimというヴァージョンマネージャーがあるらしいのですが,わたしはこの誘導をガン無視して Nix から導入しました.Nixは純粋関数型DSLで宣言的にパッケージを書けるパッケージマネージャーで,パッケージを書く要領で「コンパイラとビルドツール,フォーマッタ,Language serverなどが揃ったシェル環境」みたいなものを定義して使うこともでき,むしろ私は開発環境を整える目的で利用しています.
あまりNixについて書きすぎるとNixまで存在しない技術になってしまって困るので flake.nix
ファイルだけ置いておきます.Nix Flakesというexperimental機能を使っているなど説明が面倒くさい.
{
description = "A very basic flake";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system}; in
rec {
packages = flake-utils.lib.flattenTree
{
nim-play = pkgs.nimPackages.buildNimPackage {
name = "nim-play";
src = ./.;
};
};
defaultPackage = packages.nim-play;
apps.nim-play = flake-utils.lib.mkApp { drv = packages.nim-play; };
defaultApp = apps.nim-play;
devShell = pkgs.mkShell {
buildInputs = with pkgs; [ nim-unwrapped nimble-unwrapped nimlsp ];
};
}
);
}
buildInputs
に [ nim-unwrapped nimble-unwrapped nimlsp ]
というのを入れていますね.この flake.nix
があるディレクトリで (必要なexperimental featuresを有効にして) nix develop
というコマンドを叩くと,Nimコンパイラ nim
,ビルドツール nimble
,language server nimlsp
が導入されPATHも通った開発環境がセットアップされます.なかなか体験が良いので冬の間に記事書きたいです.
なお,Nimパッケージを収容する nimPackages
だとか buildNimPackage
関数といったものが用意されていますが,これはNimの知名度を考えると手厚く,不思議に思いました.どこかにNixとNimのおたくがいるのかもしれません.
かなり雑な機能紹介
標準入力から名前を受け取るようなプログラムは以下のように書ける.
echo "Tell me your name: "
# 標準入力
let name = readline(stdin)
echo "Hello, " & name & "!"
let name_len = if len(name) > 5:
"long"
else:
"short"
echo "Your name is " & name_len & ", " & name
これだけでいろいろと特徴がわかる:
- 変数宣言は
let
readline(stdin)
で標準入力から取れるif
式がある- 文字列結合は
&
など.if
式があるのはかなり意外でした.Pythonのような言語,とよく宣伝されていますが,酷似してるというほどではないようです.
変数宣言はSwiftと同じ体系で let
を var
に変えると再代入不可能になるやつです.
var name_len: string
if len(name) > 4:
name_len = "long"
else:
name_len = "short"
const
で定数宣言.もちろんコンパイル時に計算されます.そして when
という制御構造はコンパイル時に計算される if
です.この例では compileBadCode
を true にすると when
の中がコンパイルされるようになり,readline
の結果はコンパイル時に確定しないので const
に入れられない旨のエラーが出ます.
const compileBadCode = false
let legs = 400
when compileBadCode:
const input = readline(stdin)
静的型付けなのでオブジェクト型 (Rustでいうstructに近そう) を定義して使うこともできます.
type Person = object
name: string
age: int
let p = Person(name: "John", age: 36)
func introduce(p: Person): string =
"I am " & p.name & ". I'm " & $p.age & " yrs old."
echo introduce(p)
$
という前置演算子を使って数値などを文字列に変換します.
文字列をミュータブルに結合する例.
type Person = object
name: string
age: int
var person: Person
person.name = "Rin"
person.age = 16
var str = newString(0)
add(str, person.name)
add(str, ", you are ")
addInt(str, person.age)
add(str, " years old - right?")
echo str
ところで,
str.add(person.name)
str.add(", you are ")
str.addInt(person.age)
str.add(" years old - right?")
このようにも書ける.これがUFCS.f(a, b, c)
を a.f(b, c)
と書ける.一気にCっぽいコードからOOPっぽさが増してきた.ちなみに補完はちゃんと効く.ピリオドの後ろには識別子しか来ないから妥当か.
proc
でプロシージャ定義.Cでいう関数.面白いのが result
という変数に代入すると暗黙的にそれが返されるという機能.手続き型プログラミングの簡素さを求めたみたいな感じ.
proc sumEven(x: int, y: int): int =
for i in x..y:
if i mod 2 == 0:
result = result + i
echo(sumEven(1, 100))
for i in x..y
という構文が出てきた.Nimのforはイテレータを回す構文で,x..y
は countup(1, 10)
と同じで,以下のようなジェネレータ感ある構文で実装できます.
iterator countUp(x, y: int): int =
var i = x;
while i <= y:
yield i;
++i
おっと,++
演算子はNim組み込みでは提供されていません.Nimの演算子はただのプロシージャで,自前のものを定義することもできます.
iterator countUp(x, y: int): int =
var i = x;
while i <= y:
yield i;
++i
proc `++`(n: var int): void =
n = n + 1
おっとっと,Nimでは前方で定義されたものは参照できません (マクロがあるかららしい,コンパイラの改良でどうにかなるかもしれないということが書いてあるのは良いと思う).その代わり,明示的に宣言することで前方参照ができます.このような例はともかくとして相互再帰などの場合に便利.
proc `++`(n: var int): void
iterator countUp(x, y: int): int =
var i = x;
while i <= y:
yield i;
++i
proc `++`(n: var int): void =
n = n + 1
中置演算子として使えるキーワードがある.and
mod
など.
for i in countup(1, 100):
echo if i mod 3 == 0 and i mod 5 == 0:
"fizz buzz"
elif i mod 3 == 0:
"fizz"
elif i mod 5 == 0:
"buzz"
else:
$i
参照型はこのようにして作る.
type RefInt = ref object
value: int
let rx = RefInt(value: 3)
let ry = rx
echo rx.value
echo ry.value
rx.value = 2
echo rx.value
echo ry.value # 2
ざっとチュートリアルIから面白そうなところを抜き出してきました.うっすらとした雰囲気感がわかりますね.
好きなところ
UFCSがマジで良い.
たとえば型の相互変換にも:
# distinctつけると相互変換の必要がある
type Meters = distinct float
proc say(length: Meters) = echo length.int, "[m]"
let size = 200.Meters
# ここでは括弧は省略できる
size.say
関数や echo
に対しても:
let sq: seq[int] = @[1, 2, 3, 4]
sq.len.echo
統一的に .
を使った読みやすいコードを書けるというのがとてもエレガントだと感じました.探索すべき名前空間がより「開いて」いるぶんクラスなどと比べて補完が弱いのではないかという危惧もありましたが,(ややもっさりしている気がするものの) しっかりと補完が出てくれてうれしい.Bind-this Operator という f: (this: T, a: A, b: B) => unknown
のような関数を t::f(a, b)
と呼べるという,ちょっと汎用性の低いUFCSみたいな感じのECMAScriptプロポーザルがあり,Nimで補完がちゃんと効くということはTypeScriptには同等のサポートが期待できそうですね.
あと関数に {.noSideEffect, gcsafe, locks: 0.}
みたいなプラグマがくっついてるのは見た目がヤバくてちょっと好きです.
微妙なところ
- エディタサポートが貧弱.vimでnimsuggestがクラッシュするのはもちろん,VS Codeでもエラーがエディタ上で見れないなどかなり残念です.この点,僕がついこの間入門したGoは補完の仕方まで洗練されていて非常に良かったです.やはり金ですかね?
- 代数的データ型がない.現実世界のいろいろな事象をモデリングするにはenumは機能不足です.そもそもそういうアプリケーション層的なことをするための言語ではないのかもしれません.
- 型推論が弱いっぽい.
let sq = @[1, 2, 3, 4]
でも推論が効きませんでした.なにか曖昧性とかありましたっけ.型推論はバチバチに効いててほしい派なので困ります. - 型の抽象化が弱いらしい.conceptというinterface相当があるが使用できる場面が少ないらしい.やってないのであまり言えることがないのですが,トレイト相当を期待してはいけないみたいです.
まとめ
UFCSがこれ以上なく単純だからこそあらゆる場面で利用できることが強い.「オブジェクト指向オブジェクト指向って言ったってどうせお前らが欲しいのは obj.func
で関数が呼べて補完が効く文法なんだろ?」という姿勢はGoやRustよりも過激で最高だと思います.UFCSある言語作ってみたいですね.
自分が実用するかというと微妙です.Go敬遠してたんですけどほんのちょっとしたCLI作るのに入門したらエディタサポートに惚れちゃって2るので,抽象度の高い型がいらない用途だとGo使っちゃいそう.あまりCと連携したいようなこともないし.まあ自分から作っていけばよいのですけれども.