Skip to content

Instantly share code, notes, and snippets.

@AumyF
Last active August 2, 2022 13:01
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AumyF/7efdafa74e42853d144c29772c9aab7f to your computer and use it in GitHub Desktop.
Save AumyF/7efdafa74e42853d144c29772c9aab7f to your computer and use it in GitHub Desktop.
Pythonみたいな文法でCにトランスパイルされる言語に入門

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と同じ体系で letvar に変えると再代入不可能になるやつです.

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..ycountup(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と連携したいようなこともないし.まあ自分から作っていけばよいのですけれども.

Footnotes

  1. Goのエラー処理がいけてないという意見はわかるが真にいけてるのはResult/Eitherであって例外もいけてない,可変変数が let mut と冗長なRustについて「定数ライクに使いたい機会なんて殆どない」と評しておきながらGoではimmutable変数がないことを槍玉に上げている,cargo run を無視して「Rustには『コンパイルして実行』がない」と主張する,など.過激な切り口でものを言えば面白いと思ってしまうのは私にもある程度当てはまるので他山の石とするということで

  2. 日付フォーマットで冷めた.あれは正気の沙汰とは思えない

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment