初心者
Nim

Nim に入門してみて予想通り最高のプログラミング体験ができている件 (1) ~ 変数・制御構文・プロシージャ編

この記事について

GW 暇だったので、かねてより気になっていた Nim と戯れてみました。Nim の言語機能の一部を簡単にご紹介します。

下記の記事とそのリンク先に、基本的な情報はほとんど載っていますので、まだの方はそちらもどうぞ。

至高の言語、Nimを始めるエンジニアへ - Qiita

本シリーズ記事では Nim by Example のお題に沿って進め、後半にはプラスαで、外部モジュールの使い方なども書く予定です。

本編(第1回)には以下の項目が含まれます。
- 変数
- 制御構文
- プロシージャ

Nim の特徴

  • 静的型付け、型推論あり
  • インデントによりブロックを分ける(Python っぽい)
  • C/C++, Objective-C, JavaScript へコンパイル可能
  • C/C++, Objective-C のライブラリをインポートして使える
  • 変数宣言: const, let, var (JavaScript に似てますが、若干違います)の3種類
  • プロシージャ宣言: proc キーワードで宣言する
  • 演算子もプロシージャなので自前で定義可能
  • type alias, procedural type, exception, generics, async/await, defer, template, macro, etc.

ちょっと変態なところもあるけど、書いてて楽しい言語です。

環境

OS: macOS High Sierra
Nim: 0.18.0
Editor: Sublime Text3 w/ NimLime package

インストール

$ brew install nim

Examples

1. Hello, world

hello.nim
echo "Hello, world!"

コンパイル&実行

$ nim c -r hello.nim

パラメータは、

  • c = compile
  • -r = --run

の短縮形です。

--run をつけると、コンパイル後に実行します。

2. 変数

2.1. 変数の出力

いわゆる printf デバッグ的なのを最初に覚えておくとこの先捗ると思います。

単一の型のみの出力はいいんですが、文字列型に他の型の変数を連結して表示するような場合は $ 演算子を使って文字列に変換するか、 repr 関数を通して文字列表現を手に入れます。

# int -> string
var n = 1
echo "n=" & $n

# sequence
var members = @["John", "Paul"]
add(members, "George")
echo "members=" & $members
# or
echo "members=" & repr(members)

2.2. let: イミュータブル変数

Nim には3つの変数宣言があります。

let で宣言された変数は、再代入および破壊的操作はできません。

let n = 1
n = 2 # compile error

let members = @["John", "Paul"]
add(members, "George") # compile error

2.3. var: ミュータブル変数

var で宣言された変数は、再代入および破壊的操作ができます。

var n = 1
n = 2

var members = @["John", "Paul"]
add(members, "George") # compile error

2.4. const: 定数

const で宣言された変数は、再代入および破壊的操作はできません。

let との違いは、コンパイル時に値を決定する点なので、コンパイル時に中身が決まるのであればこちらを使いましょう。

## int
const n = 1
echo "n=" & $n

## array
const members = ["John", "Paul"]

コンパイル時に値が決まればいいので、関数の戻り値にも const が使えます。

proc getAlphabet(): string =
  var accm = ""
  for letter in 'a'..'z':  # see iterators
    accm.add(letter)
  return accm

# Computed at compilation time
const alphabet = getAlphabet()

echo alphabet
# >> abcdefghijklmnopqrstuvwxyz

# Nim by Example より抜粋

賢いですね。

2.5. 型推論

Nim は静的型付けですが、型推論してくれるので、初期化時にいちいち型を指定しなくていいようになっています。

# 型を指定する
var n: int = 1
let s: string = "Hello"

# 型推論させる
var n = 1
let s = "Hello"
n = "2" # compile error
# type mismatch: got <string> but expected 'int'

2.6. result

Nim の面白機能のひとつに result という特殊な変数があります。
これは宣言しなくても自動的に関数の戻り値になる、というものです。

proc getAlphabet(): string =
  result = ""
  for letter in 'a'..'z':
    result.add(letter)

むしろ宣言すると正しく動作しなくなるので、お気をつけください。

proc getAlphabet(): string =
  var result = ""
  for letter in 'a'..'z':
    result.add(letter)

# Warning: Special variable 'result' is shadowed.

個人的には極力使わない方がいいんじゃないかと思ってますが。

3. 制御構文

3.1. if, elif, else

let n = 10
if n == 0:
  echo "zero"
elif n mod 2 == 0:
  echo "even"
else:
  echo "odd"
# even

文 (statement) と分類されていますが、値を返します。

なので、こういう書き方も可能。

let n = 0
let s = if n == 0: "zero"
        elif 10 mod 2 == 0: "even"
        else: "odd"

echo s
# zero

3.2. case

他の言語の switch みたいなやつ。 if 同様、値を返します。

let num = -2
let s = case num:
  of low(int).. -1:
    "negative"
  of 0:
    "zero"
  of 1..high(int):
    "positive"
  else:
    "impossible"

echo s
# negative

3.3. for

ちょっと特徴的なループ構文。

# 単純なループ( 10 も含むので注意)
for i in 0..10:
  echo i

# こちらは 9 まで
for i in 0..<10:
  echo i

# array
for n in [1, 2, 3, 4, 5]:
  echo n

# 添字付き
for i, n in [1, 2, 3, 4, 5]:
  echo $i & ":" & $n

# table
import tables
for id, name in {1: "John", 2: "Paul"}.toTable:
  echo $id & ":" & $name

3.4. while

import random, strutiles

randomize()
let answer = rand(9) # 0..9
var i = 0
while true:
  var guess: int
  try:
    guess = parseInt(stdin.readLine)
  except ValueError:
    echo "Enter number[0-9]"
    continue
  if guess == answer:
    echo "Correct!" & " You have tried " & `$`(i + 1) & " times."
    break
  elif guess < answer:
    echo "Too low, try again"
  else:
    echo "Too high, try again"
  i.inc

4. プロシージャ

4.1. プロシージャ

proc fizzBuzz(n: int): string = 
  if n mod 15 == 0: "FizzBuzz"
  elif n mod 3 == 0: "Fizz"
  elif n mod 5 == 0: "Buzz"
  else: $n

for n in 1..100:
  echo fizzBuzz(n)
  • proc で始める
  • : の後に型がくる
  • 末尾に = を書いてブロック開始
  • 戻り値は return で明示的に指定することもできるが、最後に評価された式の返す値が返る
  • 引数は参照渡しでイミュータブルになるが、 var を書くとミュータブルにできる
  • 自動的に戻り値となる result 変数がある(前述)

特筆すべきは、プロシージャ呼び出しには2種類ありまして、

# 引数として
fizzBuzz(n)
# OOPっぽく
n.fizzBuzz

2番目の書き方だと、第1引数を前に持ってきて . でつなぐ形になります。

前述の

parseInt(stdin.readLine)
# parseInt(readLine(stdin)) と同じ

みたいに、関数呼び出しが入れ子になるようなときに使うのがいいかと思います。

4.2. オペレータ

generics を使ったオペレータの例です。

記号をバックティックで囲んで定義します。

proc `->`[A, B](a: A, b: B): tuple[fst: A, snd: B] =
  (a, b)

echo "hello" -> "world"
# (fst: "hello", snd: "world")

あんまりいいサンプルじゃないですし、オペレータ自作するのはほどほどにしといた方がいい気もするので(記号を見て何をするかある程度明らかな場合ならいいですが)、まぁ、こんなこともできるんだね、ていうことだけ頭の片隅に入れておけばいいかと思います。

5. 第一級関数

昨今の言語はたいていそうですが、Nim でも関数は第一級オブジェクトです。

do notation を使った書き方がちょっと混乱します。

import sequtils

let nums = @[1, 2, 3, 4, 5]
# 普通の書き方
echo filter(nums, proc (n: int): bool = n mod 2 == 0)
# do notation を使った書き方
echo filter(nums) do (n: int) -> bool: n mod 2 == 0

## 引数にプロシージャを渡す
proc even(n: int): bool = n mod 2 == 0
echo filter(nums, even)

おわりに

至高、はやや言い過ぎかとも思いますが、控えめに言って最高の体験をさせてもらっています。

どんなところがそう感じさせるのか、というのはまだはっきりと言語化できていませんが、オブジェクト指向でもなく、関数型でもなく、両方の特性をちょっとずつ取り込んだ、柔軟な言語だな、という印象は持ちました。

まだ最新バージョンが、 0.18 と若いので、今後が楽しみです。

記事内に間違いなどありましたら、コメント欄にてご指摘いただけると助かります :bow:

次回は型とコレクションについて各予定です。
:arrow_down: 書きました
Nim に入門してみて予想通り最高のプログラミング体験ができている件 (2) ~ 型・コレクション - Qiita

5/17(木) にもくもく会もあるのでそちらも楽しみです。
Nimもくもく会 - connpass