Skip to content

Instantly share code, notes, and snippets.

@miyakogi
Last active July 22, 2024 04:45
Show Gist options
  • Save miyakogi/b1df00c8bc99927d9d0d to your computer and use it in GitHub Desktop.
Save miyakogi/b1df00c8bc99927d9d0d to your computer and use it in GitHub Desktop.
Syntax of Nim

初めに

本稿ではNim言語 (旧称: Nimrod) の構文を紹介します。

Nim言語の概要やインストール方法については、ブログ記事があるのでそちらをご参照下さい。

断り書き

筆者はNimの開発者ではなく、本稿も公式なドキュメントではありません。本稿は筆者がNimのチュートリアルを読みながら書いたメモ書きを基にしており、内容には間違いが含まれている可能性があります。Nimは正式リリース前であり、今後仕様が変更される可能性があります。したがって、本稿の内容と実際の仕様・実装と異なる可能性もあります。間違いや仕様変更への遅れなどがありましたらご指摘いただけると幸いです。

以下の資料を参考にしていますが、翻訳ではありません。また、サンプルコードをそのまま、または一部改変して使用させて頂いております。問題がある場合はご指摘いただければ幸いです。

文字列、標準入出力

文字列(string型)はダブルクォート"で囲んで表現します。 シングルクォート'で囲んでも文字列にはならないので注意して下さい。

文字列は、echo 文または echo() 関数で標準出力に表示できます。

echo("hello")  # >>> hello
echo "hello"   # >>> hello
echo('hello')  # これはコンパイル時エラー!!!

ざっと見た限りでは、echoは文と関数どちらの表記を推奨しているのか判断つきませんでした。 マニュアルでも両方使われている(文の方が多く、関数形式はif文やcase文の中が多い?)ので、case-by-caseでいいのでは無いでしょうか。 関数形式の方が型変換や演算順序などで混乱が起きにくい気がしました。

また、Pythonなどと同様に、三重のダブルクォート(""" ... """)で囲むことで、改行を含む文字列を表現することができます。

echo("""Output
multiple
lines""")

# >>> Output
# multiple
# lines
# 

標準入力の取得はreadLine(stdin)でできます。 チュートリアルのコードを引用し、ついでにマルチバイト文字にしてみました。

# これはコメントです
echo("あなたのお名前は?")
var name: string = readLine(stdin)
echo("こんにちは、", name, "さん!")

このコードをgreeting.nimに書いてnim c -r greeting.nimすると

あなたのお名前は?
みやこ
こんにちは、みやこさん!

という感じで心安らぐ挨拶が交わせます

当然ですが、ファイルはUTF-8で保存してください。Windowsのコマンドプロンプトで正常に動作するかは確認していません。もし上手く動かなくて修正が必要になった時、中間のCのコードが存在するのは便利ですねだぶん。

最後の行はecho文で書くこともできます。 以下の例では、&演算子を使い文字列を連結してから表示しています。

echo "こんにちは、" & name & "さん!"

変数・定数 (var, const, let)

三種類の宣言があります。

var

変更可能 (mutable)な変数を宣言します。 型を宣言する必要がありますが、同時に代入を行って型が推論可能な場合は型の指定を省略できます。

var i: int  # int型の変数の宣言、0で初期化される
echo i      # >>> 0
i = 10      # 違う値(int型)を代入
echo i      # >>> 10

var x = 1  # 型推論でint型と判定、1で初期化
echo x     # >>> 1
x = 2      # OK
echo x     # >>> 2

i = "a"  # 異なる型(string)の代入なのでエラー
x = 1.1  # 同じく型が違う(float)のでエラー

複数行でまとめて宣言・初期化することも可能です。 ブロックはインデント(2スペース)で表されます。

var
  a, b: int  # aとbを同時にint型として宣言
  c, d = 2, 3  # cとdに同時に代入

この記法は次に述べる const, let でも可能です。

const

変更不可な定数です。宣言時に初期値を与える必要があり、型だけ指定した宣言(const i: intなど)はエラーになります。

const C = 1  # OK
echo("C=", C)  # >>> C=C
const I: int  # 初期値が与えられていないのでエラー

let

これもconstと同じく変更できません。宣言時に初期値を与える必要もあります。しかし、constとは違ってコンパイル時に値が決定されている必要はありません

let l = 1  # OK
echo l  # >>> 1
l = 3  # 値の変更はできないのでエラー
let m: int  # 初期値が与えられていないのでエラー

letconstの違いはコンパイル時に決定されるか否かです。 公式マニュアルの標準入力を使ったletの例がわかりやすかったので引用します。

const input = readLine(stdin) # Error: constant expression expected
let input = readLine(stdin)   # works

条件分岐 (if, case, 他)

if-elif-else

構文はPythonと同じで、行末にはコロンが必要です。 ブロックはインデント(2スペース)で表します。 各ブロックはインデントの深さで区別されます。 真偽値は小文字でtrue/falseです。

let a = 1
if a == 0:
  echo "ゼロ"
elif a == 1:
  echo "いち"
else:
  echo("なにこれ→ ", a)

case-of-else

Pythonには無いcase文。 私はPythonでcase文がほしいと思ったことはあまりありません(elifをたくさん書くのと違いがわからないのです)が、同じ変数に対してelifを繰り返すよりも変数 == を書かずにすむので便利かもしれません。

if文のコードをcase文で書き直します。

let b = 1
case b
of 0:
  echo "ゼロ"
of 1:
  echo "いち"
else:
  echo("なにこれ→ ", b)

上記のコードからわかるように、某スクリプト言語のswitch文のようなエグい振る舞いはしないので、毎回breakを書いたりする必要はありません。

真偽値

真偽値(bool)はtrue/falseの二値です。 否定はnotで、and, or, xorはそのままand, or, xorです。

boolへの暗黙の型変換は行われないので、bool以外の型("true", 0, nilなど)を条件に使うとコンパイル時エラーになります。

その他 (when)

when

ifの代わりにwhenもあります。 when-elif-elseで構文はifを使った場合と同じです。 ただし、各条件式はコンパイル時評価が可能でなくてはなりません。 要するにC言語の#ifdefのような感じで、プラットフォーム毎に異なるコード(例えばWindowsとLinuxで処理を分けるとか)を書きたい時にはifよりも便利だと思います。 また、whin false:でブロックをまるごとコメントアウトのようなことができて便利とのことです。

参考: 公式チュートリアル: When statement

繰り返し(while, for)

while, forともに構文はPythonと同じです。

while, break, continue

var a = 0
while a < 10:
  a = a + 1
  echo a
# (1 ~ 10まで一行ずつ出力される)

breakcontinueも使えます。

var b = 0
while b < 10:
  b = b + 1
  if b == 5:
    continue
  if b == 7:
    break
  echo b
# (1, 2, 3, 4, 6 と一行ずつ出力される)

for, break, continue

先ほとのwhileのコードをforループで書き直してみます。

for i in countup(1, 10):
  echo(i)
  # (1 ~ 10まで一行ずつ出力される)

countupは第一引数と第二引数に開始値と終了値を入れます。 第二引数の方が小さい場合はエラーになるので、countdown関数を使いましょう。 どちらの関数も第三引数でstepサイズを与える事もできます。 返す値はiteratorなので、Pythonのrangeにそっくりですね(当然ここでのPythonも3.0以上のことを指しています)。

countup(1, 10)の部分は1..10と書くこともできます。また、while同様continuebreakによるフロー制御も可能です。

for b in 1..10:
  if b == 5:
    continue
  if b == 7:
    break
  echo b
# (1, 2, 3, 4, 6 と一行ずつ出力される)

inの後にはとりあえずiteratorを置いておけばいいようですが、iterator周りはまだよく理解できていません。

スコープとblock

for, whileのブロック内は外側と別スコープです。 内側から外側の変数を参照することはできますが、逆はできません。

# OK
var a = "a"
for c in 1..2:
  echo a

# Error
for c in 1..2:
  var d = "d"
echo d  # Error!

forwhileや関数を使わなくても、blockでもブロックを作ることができ、スコープを分離できます。 したがって、以下のコードは3行目のdが未定義のためエラーになります。

block:
  var d = "d"
echo d  # Error!

block内ではbreakが使えます。 blockにlabelをつけることもでき(break mylabel:)、break mylabelなどとすることで特定のblockから抜けることもできます。 while, forなどと組み合わせることで複雑な制御フローも記述できそうですね(あまり複雑にはしないほうがいいと思いますが)。

block large:
  for i in 1..10:
    for j in 1..10:
      echo(i, j)
      if j==3:
        break  # 内側のforルールからしか抜けない
# 11, 12, 13, 21, 22, 23, ..., 103 と出力

block large:
  for i in 1..10:
    for j in 1..10:
      echo(i, j)
      if j==3:
        break large  # largeブロックを抜ける
# 11, 12, 13 だけ出力

参考: 公式チュートリアル: Scopes and the block statement

Procedure (いわゆる関数、メソッド)

Procedure の構文

宣言は proc プロシージャ名(変数1: 型, 変数2: 型, ...): 戻り値の型 = です。 この後に改行してインデントされたブロックに処理を記述します。 記号が少し違いますが、概ねPythonの関数アノテーション(例えばmypy)と同じ記法です。

例えばフィボナッチ数列を返すプロシージャは以下のように書けます。

proc fib(n: int): int =
  if n < 2:
    return n
  else:
    return fib(n - 1) + fib(n - 2)

また、短い処理であれば処理も同じ行にまとめて以下のように一行で書くこともできます。

proc isPositive(i: int): bool = i >= 0

echo isPositive(3)

returnresult

上記の例ではreturn文で値を返していますが、プロシージャ内では自動的にresultという変数が戻り値として指定された型で宣言されています。returnされずに関数の最後に到達した場合や、returnとだけ書かれた行に到達した場合、自動的にresultの値が返されます。

したがって、上記のフィボナッチ数列のプロシージャは以下のように書くこともできます。

proc fib(n: int): int =
  if n < 2:
    result = n
  else:
    result = fib(n - 1) + fib(n - 2)

Procedure の呼び出し

二つの方法があります。

一つは一般的な proc_name(arg1, arg2, arg3, ...) の形です。 上記のコードでもこの方法を使っています。

もうひとつは、いわゆるメソッド呼び出しの様に arg1.proc_name(arg2, arg3, ...) と呼び出す形です。 この時、.の前の値がプロシージャの第一引数となり、カッコの中の引数が二つ目以降の引数として扱われます。 プロシージャが引数を一つしか取らない場合、括弧は省略して arg1.proc_name と書くこともできます。

二つ目の書き方は始め違和感を覚えたのですが、プロシージャの第一引数は型が決まっていて他の型には使えないので、プロシージャを定義することは第一引数の型にメソッドを追加するのと同じだと考えると納得出来ました。静的型付言語だと当たり前なのかもしれませんが、動的型付言語ばかりやっていたので非常に新鮮でした。

引数

位置引数 と 名前付き引数

プロシージャの引数として値だけ与えられた時は引数が定義された順に扱われます。 また、引数名=値とすることで順序を無視することができます。

proc proc_name(a: int, b: string): string =
  result = ""
  for i in 1..a:
    result = result & b

echo(proc_name(10, "b"))  # >>> bbbbbbbbbb
echo(proc_name(b="b", a=10))  # >>> bbbbbbbbbb
デフォルト引数

以下の様に引数のデフォルト値を与えることもできます。

proc proc_name2(a: int, b: string = "a"): string =
  result = ""
  for i in 1..a:
    result = result & b

echo(proc_name2(10))  # >>> aaaaaaaaaa

Discard

前述したように、プロシージャは return 文がなくても必ず result の値を返します。 したがって、以下の様に戻り値が不要な関数を呼び出す時には、戻り値を"捨てる"ことを明示しなくてはなりません。 そのために discard を使用します。

proc proc_name3(a: int, b: string = "a"): string =
  var c = ""
  for i in 1..a:
    c = c & b
  echo c

discard proc_name3(10, "c")  # >>> cccccccccc
proc_name3(10, "c")  # ERROR!

また、この discard 文と複数行文字列("""文字列""")を利用することで、複数行のコメントにできます。

discard """これは
複数行の
コメント"""

宣言と実装の分離

プロシージャは使われる前に宣言されなければなりません。 したがって、以下のコードはエラーになります。

echo(proc_name5)  # Error! Undefined identifier: 'proc_name5'

proc proc_name5(a: int, b: string = "a"): string =
  result = ""
  for i in 1..a:
    result = result & b

先に宣言を書くことで、このエラーを回避できます。実装を伴わない宣言では、最後の=を書きません。

proc proc_name5(a: int, b: string = "a"): string

echo(proc_name5(10))  # OK!

proc proc_name5(a: int, b: string = "a"): string =
  result = ""
  for i in 1..a:
    result = result & b

上記の例では有用性がありませんが、公式のチュートリアルにより現実的な例があります。

無名関数

無名関数も使えます。 nim-by-exampleの例をそのまま借ります。

import sequtils

let powersOfTwo = @[1, 2, 4, 8, 16, 32, 64, 128, 256]

echo powersOfTwo.filter(proc (x: int): bool = x > 32)  # 無名関数1
echo powersOfTwo.filter do (x: int) -> bool : x > 32   # 無名関数2

@[...] はSeqs型 (sequence) のエイリアスです。 Seqs型は可変長の配列(リスト)で、要素の型が全て同一のものです。

無名関数の書き方はコード例の5, 6行目の二通りあります。(5行目のように)procで書く場合は、プロシージャを一行で書く記法とほぼ同じで、プロシージャ名を省いたものです。もうひとつはdoを使った書き方で、こちらの方が多少短く書けます(が、個人的にはわかりにくい気がします)。

Nim by Exampleには、普通に定義されたプロシージャを上の様に引数にすることはできない、と書いてありますが、私が試した所、普通に定義したプロシージャ({.procvar.} pragmaなし)でも名前を引数に使えました。

Pragma

プロシージャの定義時、戻り値と=の間に {.文字列.} で囲まれたpragmaを書くことができます。これによって、コンパイル時にコンパイラに様々な指示を与えることができるそうです。詳細はマニュアルを参照して下さい。

Discardable pragma

discardable pragmaを使用すると、discardしなくてもエラーではなくなります。

proc proc_name4(a: int, b: string = "a"): string {.discardable.} =
  var c = ""
  for i in 1..a:
    c = c & b
  echo c

discard proc_name4(10, "c")  # >>> cccccccccc
proc_name4(10, "c")  # 今度はOK!!
noSideEffect pragma

noSideEffect pragmaを使用すると、プロシージャが副作用を伴う時エラーになるらしいのですが、手元では適当な例で確認できませんでした。 以下のコードはnim-by-exampleでSide effectの例として書かれていたものですが、普通に実行できました。

proc sum(x, y: int): int {. noSideEffect .} =
  x + y

proc minus(x, y: int): int {. noSideEffect .} =
  echo x  # error: 'minus' can have side effects
  x - y

参考: http://nim-lang.org/manual.html#nosideeffect-pragma

その他

他にもpragmaはたくさんあります。

参考: http://nim-lang.org/manual.html#pragmas

Iterators

プロシージャに似たものにイテレータがあります。 Pythonのイテレータのようなものだと思います。

組み込みの countup 相当のイテレータの定義は以下のようになります。

iterator myCountup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

for i in myCountup(1, 7):
  echo i

プロシージャとの違いは以下のとおりです。

  • 宣言に proc ではなく iterator を使う
  • return ではなく yield を使う
  • forループからの呼び出しが可能 (※要確認、nextのような関数はない?)
  • result の暗黙的な宣言は行われない
  • その他、詳細はチュートリアルなどを参照して下さい

基本的な型には、真偽値、Character型、文字列型、整数型、浮動小数点型などがあります。他にArrayやSet, Enumなどがあります。

真偽値

truefalse の二値のみです。

Character型

1バイトの文字です。シングルクォート(')で囲みます。

var a: char = 'a'
echo a  # >>> a
var b: char = 'ss' # エラー
var b: char = 'あ' # エラー

文字列型

いわゆる文字列です。ダブルクォート(")で囲んで定義します。

var s: string = "String"
echo s  # >>> String
echo(s.len())  # >>> 6 長さも求められます
for i in s:  # シーケンシャルアクセスも可能です
    echo i  # 一行ずつS, t, r, i, n, gと出力

初期化していない場合は nil になります。 空文字列 "" ではありません。

var s: string
if s == nil:
  echo "nil"
else:
  echo "string"

# >>> nil

& 演算子で文字列同士を連結できます。PythonやJavaScriptなどとは違い、デフォルトでは+演算子で文字列を連結することはできません。

let s1 = "aaa"
let s2 = "bbb"
echo s1 & s2  # >>> aaabbb
# echo s1 + s2  # Error!

マルチバイト文字も扱えますが、長さなどは正確に求められません。そんな時はunicodeモジュールを使うとなんとかなるかもしれません。

var a: string = "文字列"
echo a  # >>> 文字列
echo(a.len())  # 9 ← bytee数で数えられてしまう

from unicode import runeLen  # unicodeモジュールには他にも便利な関数があるそうです
echo(a.runeLen())  # 3

unicodeといいますか、多言語のサポートはまだ十分ではないように感じます。 特にunicode以外のマルチバイト文字の扱いがよくわかりませんでした。 Windowsで日本語パス名を扱う時や、コマンドプロンプトで日本語入力を受ける時にどうなるのかは未確認です。

整数型

以下の10種類です。

int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64

特に指定がなければintが使用されます。以下のようにシングルクォート(')を使ってsuffixで型を指定することもできます。

let
  a = 1  # int
  x = 5'i8  # int8
  y = 143'i64  # int64
  u = 0'u  # uint

浮動小数点型

以下の3種類です。

float float32 float64

特に指定がなければfloatが使用されます。整数型と同様、suffixで型を指定することもできます。

let x = 0.0'f32  # float32

Enum型

Pythonでも3.4で導入されたEnum型ですが、私はよく理解できていません。

Subrange

整数型またはEnum型の一部範囲から構成される型(subrange)を定義できます。 以下の例ではSubInt型を 0 ~ 5 の6つの整数からなる型として定義しています。 範囲外の値にアクセスするとエラーになります。

type
  SubInt = range[0..5]

var sr: SubInt = 0
echo sr
for s in 0..10:
  sr = sr + 1
  echo sr  # 0, ..., 5まで出力され、以下のエラーが出る(実行時エラー)
  # Error: unhandled exception: value out of range: 6 [RangeError]

Array

単一の型のみで構成される固定長配列です。[]で作れます。

let arr = [1, 2, 3, 4, 5]
for s in arr:
  echo s  # 1, ..., 5 を縦に出力

type 宣内言でarray[インデックス範囲, 型]を使うことで、独自の型を定義することも可能です。

type
  # 0 ~ 5のインデックスでアクセスできる、長さ6で要素の型がintの配列型を定義
  IntArray = array[0..5, int]
  IntArray1 = array[6, int]  # 要素数だけを指定する事も可能(0始まり)
var
  x: IntArray
x = [1, 2, 3, 4, 5, 6]
for i in low(x)..high(x):
  echo(x[i])

var y: IntArray
y = [1, 2, 3, 4, 5]  # 要素数が足りないのでエラー
y = [1, 2, 3, 4, 5, 6, 7]  # 要素数が多すぎるのでエラー

インデックスは0始まりでなくてもOKです。

type
  # 10 ~ 15のインデックスでアクセスできる配列
  IntArray2 = array[10..15, int]
var z: IntArray2
z = [1, 2, 3, 4, 5, 6]
echo z[10]  # >>> 1
echo z[1]  # エラー

forループで2つの値を与えると、インデックスとそれに対応する値を返します。

# 上記のコードの続き
for i, v in z:
  echo("i=", $i, ", v=", $v)
  # >>> i=10, v=1
  # >>> i=11, v=2
  # >>> i=12, v=3
  # >>> i=13, v=4
  # >>> i=14, v=5
  # >>> i=15, v=6

インデックスとそれに対応する値を同時に取れるのは便利ですね。 Pythonだとわざわざenumerate関数を使う必要がありました。

# pythonの場合
for i, v in enumerate(range(10, 20)):
    print([i, v])  # >>> [0, 10] ... [9, 19]

Sequence

Sequence型は可変長のArrayです。 Arrayに@演算子を作用させることで作れます。 インデックスは常に0始まりで、i番の要素にアクセスする方法はArray同様x[i]です。

var x: seq[int]  # a sequence of integers
echo(repr(x))  # >>> nil 初期値はnil
x = @[1, 2, 3, 4, 5, 6]  # @でArrayをSeqに変換
echo(repr(x))  # >>> 0x7f3f1fd5b050[1, 2, 3, 4, 5, 6] なぜかメモリ番地的な出力
echo($x)  # >>> @[1, 2, 3, 4, 5, 6] これは期待通り

Array同様、forループで使えますし、indexと値を同時に取ることも可能です。

for i in @[3, 4, 5]:
  echo($i)  # >>> 3, 4, 5と順に出力

for i, v in @[3, 4, 5]:  # Arrayの様にindexと値を一度に値に入れることが可能
  echo("i=", $i, ", v=", $v)
  # >>> i=0, v=3
  # >>> i=1, v=4
  # >>> i=2, v=5

Set

数学で言う所のセットだそうです。{}で定義できます。 要素にはint, char, boolなどordinal型のみ利用可能です。

type
  TCharSet = set[char]
var
  s: TCharSet
s = {'a'..'f'}
let
  t: TCharSet = {'d'..'k'}
echo($s)  # >>> {a, b, c, d, e, f}
echo($t)  # >>> {d, e, f, g, h, i, j, k}

let u = s + t  # Union of two sets
echo($u)  # >>> {a, b, c, d, e, f, g, h, i, j, k}
let d1 = s - t  # Difference of two sets
let d2 = t - s  # Difference of two sets
echo($d1, ", ", $d2)  # >>> {a, b, c}, {g, h, i, j, k}
let i = s * t  # Intersection of two sets
echo($i) # >>> {d, e, f}

フラグ管理などに便利との事です。

Varargs

引数の数が不定なプロシージャで使われます。 引数は自動的にArrayに変換されます。 この引数は最後でなくてはなりません。

proc print(b: varargs[string]) =
  var res = ""
  for s in items(b):
    res = res & s
  echo res
print("a", "b", "c", "d")  # >>> abcd

上記のコードはitems()がなくても動きます。 itemsの説明はよく理解できませんでした。

Slice

配列の一部などを切り出すスライスです。

var
  a = [1, 2, 3, 4, 5]
  b = "abcdefghijk"

echo a[2..3] # >>> @[3, 4] Arrayの一部を取得
echo b[2..3] # >>> cd  文字列にも使える
b[2..3] = "CDDDD"  # 置き換える事もできる
echo b  # >>> abCDDDDefghijk  

Tuple

Pythonのタプルとは違います。 複数の名前付きフィールドを持つ型を定義するものです。 感覚としては、Pythonのクラスの低機能版が近いような気がします。

type
  TPerson = tuple[name: string, age: int]

var person: TPerson
person = (name: "Peter", age: 30)
# person = ("Peter", 30) でも同じ

# .field でアクセス可能
echo(person.name)  # >>> Peter
echo(person.age)  # >>> 30
# indexでもアクセス可能
echo(person[0])  # >>> Peter
echo(person[1])  # >>> 30

# type宣言なしでもOK

var personB : tuple[name: string, age: int] = ("Mark", 42)
echo($person)  # >>> (name: Peter, age: 30)
echo($personB)  # >>> (name: Mark, age: 42) 
person = personB  # OK! 同じフィールド名・タイプが同順なので同じ型扱い
echo($person)  # >>> (name: Mark, age: 42) 

Object

Tupleに似ていますが、Objectの方が多機能で、継承や隠蔽ができるそうです。 オブジェクト指向な書き方をする時、Objectがクラスに相当するのでしょうか。

Reference/pointer 型

  • 追跡される参照: reference (refで宣言)
  • 追跡されない参照: pointer (ptrで宣言)

Referenceの方が安全。 Pointerは低レベル処理に必要になるかも。

Procedural 型

Procedureへの参照(Pointer)。 ProcedureをPythonでの第一級関数の様に扱えるので、関数型言語のような書き方ができます。

無名関数の節の例を再利用します。

import sequtils

let powersOfTwo = @[1, 2, 4, 8, 16, 32, 64, 128, 256]

echo powersOfTwo.filter(proc (x: int): bool = x > 32)  # 無名関数1
echo powersOfTwo.filter do (x: int) -> bool : x > 32   # 無名関数2

proc gt32(x: int): bool =
  if x > 32:
    result = true
  else:
    result = false

echo powersOfTwo.filter(gt32)  # 引数にProcedureを与えられる
# >>> @[64, 128, 256]

型変換

文字列への変換

$演算子をつけることで文字列に変換できます。

let i = 1
# echo "String" & i  # Error
echo "String" & $i  # OK, >>> String1

repr()でも文字列に変換できます。こちらのほうが変換できる型が多いです。

let arr = [1, 2, 3, 4, 5]
# echo arr  # Error
# echo $arr  # Error
echo repr(arr)  # OK, >>> [1, 2, 3, 4, 5]

整数から浮動小数点、またはその逆の変換

toInt, toFloat を使います。toInt では値は四捨五入されます。

let i = 1
let f = 2.3
let f2 = 2.7

echo f.toInt  # >>> 2
echo f2.toInt  # >>> 3
echo i.toFloat  # >>> 1.0

文字列から数値への変換

strutilsモジュールを使います。

from strutils import parseInt, parseFloat

let
  si = "13"
  sf = "43.5"

# echo si + 3  # Error
# echo sf + 3.4  # Error
echo si.parseInt + 3  # >>> 16
echo sf.parseFloat + 3.4  # >>> 46.8

例外処理 (raise, try-except-finally)

例外はオブジェクトで、わかりやすくするために例外型にはsuffixとしてErrorがつけられます。

raise

例外は raise 文で発生させられます。

var
  e: ref OSError  # 既存の例外を参照
new(e)  # 新規にオブジェクトを作っている?のかな?
e.msg = "the request to the OS failed"  # msgプロパティに値は例外発生時に表示される
raise e  # 例外発生

# >>> Traceback (most recent call last)
# >>> file_name.nim(5)         exception
# >>> Error: unhandled exception: the request to the OS failed [OSError]
# >>> Error: execution of an external program failed

上記のコードは、system モジュールの newException テンプレートを使うことで一行で書けます。

raise newException(OSError, "the request to the OS failed")

try-except-finally

構文はだいたいPythonと同じです。

var f: File
if open(f, "sometext.txt"):
    try:
      ...  # 何か処理
    except SomeError:  # try節でSomeErrorが発生したらここ
      echo "some error"
    except AnotherError:  # AnotherErrorが発生したらここ
      echo "another error"
    except:  # その他のエラーが発生した場合はここ
      echo "unknown erro"
      raise  # raiseだけの場合、同じ例外を上に投げる
    finally:
      echo "finally"  # 必ず実行される

defer

try-finallyのようなものです。 defer文より後の同じブロック内の処理がtry節に入っているのと同じです。 defer: do-somethingdo-something部分がfinalyで実行される処理と同じです。

  • try-finally で書いた場合
var f = open("numbers.txt")
try:
  f.write "abc"
  f.write "def"
finally:
  close(f)
  • defer で書いた場合
var f = open("numbers.txt")
defer: close(f)
f.write "abc"
f.write "def"

Module, import

モジュール(別ファイル)に分割することができます。 モジュールに分割することで、名前空間を分割してプログラムを書くことができます。 他のモジュールのシンボルへはimport文によってアクセスします。 外部モジュールから参照できるシンボルは、トップレベルかつ * 付きのシンボルだけです。

# Module A
var x*, y: int

proc `*` *(a, b: seq[int]): seq[int] =
  newSeq(result, len(a))
  for i in 0..len(a)-1:
    result[i] = a[i] * b[i]

when isMainModule:
  assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9])

上記のコードでは、x* は他のモジュールでimport可能ですが、 y はimportできません。

トップレベルの宣言はプログラムの開始時に実行されます。これは、例えば複雑なデータ構造を初期化するために使うことができます。

各モジュールには特別な定数、isMainModule が定義されています。 この定数は、そのモジュールがメインファイルとしてコンパイルされた時に true になります。 これを使って、先ほどの例の様にモジュール内にテストを埋め込むことができます。

モジュールをコンパイルする流れは、以下のようになっています。

  1. いつものように、モジュール全体をコンパイルし、import文にしたがって繰り返していく
  2. パース済みのシンボルだけがexportされる。もし未知のシンボルがあれば中止する

以下の例がわかりやすいです。

# Module A
type
  T1* = int  # Module A exports the type ``T1``
import B     # the compiler starts parsing B

proc main() =
  var i = p(3) # works because B has been parsed completely here

main()
# Module B
import A  # A is not parsed here! Only the already known symbols
          # of A are imported.

proc p*(x: A.T1): A.T1 =
  # this works because the compiler has already
  # added T1 to A's interface symbol table
  result = x + 1

外部モジュールのシンボルは、module.symbol の構文ですることができます。 シンボルが(どのモジュールのものを指しているのか)曖昧な場合は、上記の構文で限定されなくてはなりません。

複数のモジュールで定義され、両方のモジュールが第三のモジュールにインポートされた時、そのシンボルは曖昧です。

# Module A
var x*: string
# Module B
var x*: int
# Module C
import A, B

write(stdout, x) # error: x is ambiguous (AかBか区別できない)
write(stdout, A.x) # no error: qualifier used (Aのxと一意に決まる)

var x = 4  # ここで自モジュールのxを作成
write(stdout, x) # not ambiguous: uses the module C's x

ただし、この規則はprocedureとiteratorには適用されない。 以下の例ではオーバーロードの規則が適用される。

# Module A
proc x*(a: int): string = $a
# Module B
proc x*(a: string): string = $a
# Module C
import A, B
write(stdout, x(3))   # no error: A.x is called
write(stdout, x(""))  # no error: B.x is called

proc x*(a: int): string = nil
write(stdout, x(3))   # ambiguous: which `x` is to call?

Except

通常の import 文はエクスポートされている全てのシンボルを取得してしまう。 これは除外するシンボルを except で指定することで制限できる。

import mymodule except y

From

単純なimport文は全てのシンボルをインポートする。インポートしたいシンボルだけを指定したい場合は、 from ... import ... を使う。

from mymodule import x, y, z

from文では、名前空間制限を強制している。したがって、モジュール名.シンボルの形式で全てのシンボルにアクセスできる。

from mymodule import x, y, z

x()           # use x without any qualification
from mymodule import nil

mymodule.x()  # must qualify x with the module name as prefix

x()           # using x here without qualification is a compile error

モジュールに別名を付けて短くする事もできる。

from mymodule as m import nil

m.x()         # m is aliasing mymodule
@hitoshi44
Copy link

通りすがりです。とても見やすくまとめていただきありがとうございます。
case文 は各々の都度判定を避けることができるので、分岐の数がとても多い場合には処理速度とメモリ効率両方の面で有用なんだと思います。

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