Nim (3)

ある方が、放送大学で行われる、 コンピューティング('15)-原理とその展開- なんてのを紹介されてた。録画して見たいと思ったら、ラジオだった。でも放送時間が、2215JSTじゃ、 オイラーはもう寝てますよ。

でも、ラジオって何KHz? 疑問に思って、FAQを 見たら、ちと残念な事が書いてあった。著作権の問題なの?それとも、技術的問題? 安倍ちゃん 何とかしてよ。

いきなり安倍ちゃんはないだろう。もっと現場に近い、文部科学省の下○さんか。 それとも、電波の停止権を持ってる、ソームのおばちゃんか。 そこまで行かなくても、 放送大学の学長あたりに、ねじこめばいいんでないかい。

学長は、 岡部 洋一先生らしい。こういうねじこみを緩和するために、 先生の受け持ち分が公開されてた。で、コンピュータうんぬんよりも、 電磁気学 FAQなんてのに目が行った。

いきなり数学が出てきたよ。電磁気学の統一理論は、 マクスウェルの方程式 に、止めを刺すのかな。わるいけど、オイラーには、チンプンカンプン。

そりゃ、(アマチュア)無線界で小型でよいとブームになってる、マグネチックループアンテナの 原理ぐらいは説明出来ますけどね。

今までのアンテナは、電界モードで働くダイポールアンテナが基本。魚の骨みたいな八木アンテナとか 避雷針みたいなバーチカルアンテナしかり。でも、電波って、正確には、電磁波の事。

電界が磁界を生み、磁界が電界を生み、電界が磁界を生みってのが繰り返されて伝わっていく。 ダイポールアンテナは、電界からスタートするけど、効率よく電界を発生するには、周波数で 決まる物理サイズの推奨値が決まってた。だから、自由な大きさのアンテナを作るのは難しかったんだ。

磁界をスタートとしたらって発想の元に作られたのが磁界ループアンテナ。いかに強い磁界を 発生させるかが鍵。そんなの簡単アンペア・ターン。 大きな電流(アンペア)と巻き数(ターン)の多いコイルがが有ればよい。コイルの巻数を 増やすと弊害があるので、ワンターンが一般的。ワンターンのループね。

電流をたくさん流すには、使う周波数で共振させてリアクタンス分をゼロにしてあげれば良い。 残るは抵抗分。そんなの太い銅線を使っとけ。太い銅線でも、高周波電流は、表皮効果で、 表面しか流れない。だったら銅のパイプでいいんでない。これが原理さ。

この共振回路へ電力を送り込む方法として普通は、M結合を使う。要するに空芯のトランスね。一次側は、 送信機に接続。二次側は、共振回路兼強磁界発生器。最近、超伝導の開発競争の話を聞かないけど、 常温超伝導材が出来たら、すばらしい効果が出るでしょうね。

いかんいかん、話が逸れた。 電磁方程式を理解するための第一歩ってのを 見つけてきたので、読んでみるかな。

件のラジオ放送だが、ラジ子でも聴けるらしい。けど、タイマー録音ってパソコンを 起動しといて、cronでも仕掛けないとだめだな。ん?、ipadにそんな機能を持ったアプリって あるの? あの板は、リアルタイムに人と戯れるように作られているからなあ。

宿題提出

前回、parseIntの簡単な例をtccで実行すると、正しく動かないって事で、逆アセンブラを 解析する事になってた。で、その回答を考えてみた。

前回は、nim語で言うと、

let res = 31.BiggestInt
if res < low(int) :
    echo("overflow")
else:
    echo("OK")

こんなんだったけど、問題を単純化する為、31をやめて、low(int)にしてみる。 じゃ、nimcacheに下りて、tccでgdbが使えるように コンパイルする。nimで唯一のインクルードファイル、nimbase.hを、nim/libから持って きておくと、楽だぞ。

[sakae@fedora nimcache]$ tcc -g system.c tc.c
[sakae@fedora nimcache]$ gdb -q a.out
   :
71              res_88005 = IL64(-2147483648);
(gdb) n
73                      if (!(res_88005 < IL64(-2147483648))) goto LA3;
(gdb) disassemble
Dump of assembler code for function tcInit:
   0x0804d15f <+0>:     push   %ebp
   0x0804d160 <+1>:     mov    %esp,%ebp
   0x0804d162 <+3>:     sub    $0x4,%esp
   0x0804d168 <+9>:     mov    $0x80000000,%eax
   0x0804d16d <+14>:    mov    $0x0,%ecx
   0x0804d172 <+19>:    mov    %eax,0x804f588
   0x0804d178 <+25>:    mov    %ecx,0x804f58c
=> 0x0804d17e <+31>:    mov    0x804f588,%eax
   0x0804d184 <+37>:    mov    0x804f58c,%ecx

32Bitの石で64Bitの演算を行うために、下位intと上位intを組み合わせるとな。インテルの 昔有った石で8086だかは、8Bitの ALレジとAHレジを合わせてAXとかいう集合で扱ってたのと一緒だな。

この場合、マイナスの値を指定してるんで、上位intは下位intのMSBが拡張されないといけなかったはず。 上記では、下位intをeaxが扱い、上位intをexcが扱っている。で、上位はZERO。64Bit全体で 見ると、正数になるな。IL64の計算が間違ってる。これじゃ、正しい判定が出来る訳がない。

ちなみに、負数を作るには、uintな正数をまず作る。その全ビットを反転する。次にプラス1するんだった。 これを、2の補数表現と言う。負数を正数にする場合も、全ビットを反転し、それに1を加えるんだった。

オイラーの勘だけど、これって、Off by One と言う、俗に言う、一つ違いのエラーではなかろうか。 あるいは、境界エラーと言う方が、この場合は正しいかな。御託を並べていないで、検証してみる。

let res = BiggestInt(low(int) + 1)

nim語で、数値を一つづらしてみた。数値で言うと、-2147483648 + 1って事になる。 これで確認すると、

   0x0804841b <+0>:     push   %ebp
   0x0804841c <+1>:     mov    %esp,%ebp
   0x0804841e <+3>:     sub    $0x4,%esp
   0x08048424 <+9>:     mov    $0x80000001,%eax
   0x08048429 <+14>:    mov    $0xffffffff,%ecx

今度は、上位intが正しくなった。tccに send PR(Bug報告かな)

それにしても、レジスタの本数が少ない糞石は、コンパイラの作者さん泣かせだな。 レジスタを壊したくないので、スタックに保存。レジスタを他の用途に使った後、スタック からデータを復帰なんて、頭が痛くなるような事をやらされているんだもん。

WindowsでもNim言語

nimのデレクトリィーツリーの中に、compilerってのとc_code ってのが有る。そのうちc_codeの方は、 インストール時にちょいと覗いてみてた。各種OS、32,64用のC語が入っていた。じゃ、compilerの方は?

覗いてみるとnim語だった。readmeによると、

This directory contains the Nim compiler written in Nim. Note that this
code has been translated from a bootstrapping version written in Pascal, so
the code is **not** a poster child of good Nim code.

Nimで書かれたNim用のコンパイラーとな。それじゃ最初にNimはどうやって吐き出した? にわとりと玉子の問題がここでも噴出。それを解決したのはパスカルさんとな。

このnim語をC語に変換したのが、c_codeの中に鎮座してるのか。興味深げにcompilerを眺めてみる。 で、たまたま行き当たったのが、extcomp.nim。この中に興味深い事が書かれていた。

# GNU C and C++ Compiler
# LLVM Frontend for GCC/G++
# Clang (LLVM) C/C++ Compiler
# Microsoft Visual C/C++ Compiler
# Intel C/C++ Compiler
# Local C Compiler
# Borland C Compiler
# Digital Mars C Compiler
# Watcom C Compiler
# Tiny C Compiler
# Pelles C Compiler
# Your C Compiler

これだけのC言語コンパイラーが、Nimが吐き出すC語をコンパイル出来ますってさ。中には、 Bugを見つけたTiny C Compilerが当然入ってた。

興味深いのは、Borland C Compilerがサポートされてる事。WindowsXPだかの時代に、無料で 使えるってんで入手して、Windows7にも持ってきてある。このコンパイラーは、コンパイルが めちゃくちゃ速いんだよな。

ならば、物は試しに、Widows7にもNimを入れてみるか。そして、Borland C Compilerが 使えるか試してみよう。

Fedoraに入ってる一式を、Windows7に転送。bin/nim.exeを作るのには、mingwで出している、 gcc 4.5.2と言う古いのを使った。

nim.exeが出来てしまえば、Disk領域を無駄に使ってる、c_code(や、compiler)を削除して しまおう。根本的に必要なのは、configとlibだけのはず。

記念に前回やったgnuplotでグラフを書くやつをやってみた。無事に動いたよ。それから、exampleの中に有った、 これがwindows用ってやつ、wingui.nimも試してみた。

# test a Windows GUI application

import
  windows, shellapi, nb30, mmsystem, shfolder

discard MessageBox(0, "Hello World!", "Nimrod GUI Application", 0)

MessageBoxからの返値は無視していいよってんで、discardが付いている。果たして何が返って くるかは、windowsモジュールに書いてあるのかな。やたら長くて見る気しないけど。

それじゃ早速bccを指定してコンパイルしてみる。

C:\app\nim\bin>nim.exe --cc:bcc -d:release c tc.nim
Hint: used config file 'C:\app\nim\config\nim.cfg' [Conf]
Hint: system [Processing]
Hint: tc [Processing]
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
c:\app\nim\bin\nimcache\tc.c:
警告 W8064 c:\app\nim\bin\nimcache\tc.c 44: プロトタイプ宣言のない関数の呼び出し(関数 PreMain )
警告 W8065 c:\app\nim\bin\nimcache\tc.c 56: プロトタイプ宣言のない関数 'PreMain' の呼び出し(関数 NimMain )
  :
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
c:\app\nim\bin\nimcache\system.c:
警告 W8027 c:\app\nim\bin\nimcache\system.c 373: gotoを含む関数はインライン展開できない
警告 W8004 c:\app\nim\bin\nimcache\system.c 399: 'result' に代入した値は使われていない(関数 intsetget_28524 )
  :
エラー E2228 c:\app\nim\bin\nimcache\system.c 2351: エラーあるいは警告が多すぎる(関数 getoccupiedmem_6495 )
*** 1 errors in Compile ***
Error: execution of an external program failed

余りに不注意が多すぎたんで、さじを投げちゃったんですかね。 tc.cの冒頭には、こんな風にコンパイルしましたってのが書いてある。

/* Compiled for: Windows, i386, bcc */
/* Command for C compiler:
   bcc32.exe -c -O2 -6   -IC:\app\nim\lib -oc:\app\nim\bin\nimcache\tc.obj c:\app\nim\bin\nimcache\tc.c 
*/

bccのマニュアルを見ると、-O2ってのがなるべく高速に、-6はPentiumPro用の命令を作ってねと、知れた。 警告は、-wでon。そいつにマイナスを付けるとoffになるらしい。ゆえに、nim.cfgに

# for bcc
bcc.options.always = "-w-"

こんな設定をしてみた。果たして動くかな?

C:\app\nim\bin>nim.exe --cc:bcc -d:release c -r tc.nim
    :
Hint: system [Processing]
Hint: tc [Processing]
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
c:\app\nim\bin\nimcache\tc.c:
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
c:\app\nim\bin\nimcache\system.c:
[Linking]
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
Turbo Incremental Link 5.00 Copyright (c) 1997, 2000 Borland
Hint: operation successful (8915 lines compiled; 0.780 sec total; 6.666MB) [SuccessX]
c:\app\nim\bin\tc.exe
overflow

動いた。そしてtccと同じ問題を抱えているな。グラフを書くアプリもちゃんと動いた。 コンパイルが速くて、なかなか良いぞ。

今はnim.exeにPATHを通してないけど(毎度、M$の糞ユーザーインターフェースに触れたくない)、 バッチファイルを作って、手軽に使えるようにしておくかな。

どうやってバッチを書く。すっかり忘れてるぞ。そんなん、インストール時に使った、install.batを 見ればいいじゃん。

getopt

さて、少しコードを書いておくか。最初に、getoptだな。そんなん、parseoptとかparseopt2が 提供されるから、それを使え。いえいえ、自前で実装してこそ身に作ってもんです。

Rustでやったのをコピペしてみる。えと、stractの代わりに使えそうなものをnimから 見繕ってくるのが、とっかかりだな。それから、コマンドラインの結果をnim側に取り込むには、 どうするの? これらが分かれば、後はコピーして、それを書き換えて行くとな。

ええ、parseoptのソースは一切参照しない、クリーンルーム開発を目指します。

import os, algorithm, strutils

type
    Opt = object of RootObj
        ap: int # 1: am, 2: pm
        tl: int # Number of data
        ss: int # stat
        pp: int # print
        lg: int # line graph
        er: int # error detect

proc getopt() : Opt =
    var args  = newSeq[string](0)
    for i in  0 .. paramCount(): args.add(paramStr(i))
    args[0] = "fin"
    result = Opt(ap:0, tl:0, ss:0, pp:0, lg:0, er:0) # no need var
    var av = 0
    for s in args.reversed():
        case s:
            of "fin":
                if paramCount() == 0: result.er = 1
                break
            of "-am": result.ap = 1
            of "-pm": result.ap = 2
            of "-ss": result.ss = 1
            of "-pp": result.pp = 1
            of "-lg": result.lg = 1
            of "-tl": result.tl = av
            else:                        # Must be number
                try: av = parseInt(s)
                except ValueError: result.er = 2

var q = getopt()
echo(q.ss)
echo(q.tl)
echo(q.er)

getopt()と言う自前関数と、その用例。コピペもとえRustからのコード変換は楽でいいなあ。

Nim語が提供するコマンドラインに関する関数は、2個有る。 paramCount()は、コマンド名は除いた、引数の数を返してくれる。もう一つは、paramStr(i)、 i番目の引数を返してくれる。

これらを使って、引数を全部取り込んでいる。paramStr(0)は、コマンド名を取れるってのは、 この業界の暗黙の了承事項かな。レンジ関数と言うやつを使って、scanしてみた。

解析は、取り込んだ引数を、例によって、右から左に向かってやってる。そのために、一度 seqをreversedでひっくり返している。これが提供されてるモジュールが、algorithmって 言う大げさな名前になってた。sortもこの中に有ったぞ。土地勘大事。いろいろ歩き回る事だ。

parthIntは間違った(数値以外)文字列を受け取ると例外を挙げてくるので、exceptでそれを キャッチして、エラーフラグを立てるようにした。

この関数の返値は、resultになる。あれ、最後に、return resultしてないじゃんと、いぶかる 向きがあろう。関数の中でresultと言う名前の変数は、結果を返すための専用名に決められている。 この変数名に限っては、var result してはいけない。内部的に変数名の登録処理が 行われている。

オイラー的には、こういう例外事項は、余計なお世話で嫌いだな。だったら、使うな。 ごもっとも。でも、人様のコードを読むのに必要になるから、試しに使ってみましたとさ。

折角書いたコードですが、一晩寝かせたら、虫が湧いてきましたよ。春ですからねぇ、夢うつつ だったのでしょうか?

exceptでキャッチする例外は、ValueErrorだけじゃ無いだろう。前回から悩まされた、OverflowErrorは、すり抜けて いっちゃうぞ。ここは、是非、一網打尽に捕まえるようにせいよ。でも、負数はすり抜けて くるなあ。

発想を変えて、正数ならhead、負数ならtail用の引数って解釈させる手があるな。そうすれば、 わざわざ -tlなんて前置詞が必要無くなるし、headも自然に実現出来る。まあ、過去のしがらみも あるから、斬新なのは却下して、absでも挟んでおくかな。

もう一つは、ささいな事だけど、finの所のbreakは必要無いね。ループが終わる事が保障 されてますから。(rustの時は、while trueで回してて、その中から、脱出するって書いてた)

小技達

上のtryあたりのマニュアルを眺めていたら、

let x = try: parseInt("133a")
        except: -1
        finally: echo "hi"

こういうトライ式(って、KUMON式に対抗してますか)は、

let x = (try: parseInt("133a") except: -1)

こういう風に、さっぱりと書けますよとな。そして、その下にあった、Goからの輸入品も 興味深い。

var f = open("numbers.txt")
try:
  f.write "abc"
  f.write "def"
finally:
  close(f)

何か良からぬ事が起こった時に、取り合えず、closeしとけって例。やる事をやって、最後に 閉めるのをとかく忘れがち。そういう時は、先におまじないを唱えておけとな。

var f = open("numbers.txt")
defer: close(f)
f.write "abc"
f.write "def"

但しこの技を使えるのは、ブロックの中だけ。Topレベルでは、無効だよって、そりゃそうだわな。 何処でcloseしたら良いかのタイミングが掴めないもの。

Templates

シンプルなマクロって事です。Lispに一歩近づいてきたな。簡単な例。

template `!=` (a, b: expr): expr =
  # this definition exists in the System module
  not (a == b)

assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))

それはそれで良いんだけど、Lispのマクロの指南本 On Lispでは、グレアム太子が、 マクロは使うな、他の手段はないか、どうしてもって場合だけ、使えって戒めている。

Nimの人達は、どんなマクロを使ってるの? 実例に当たるのが一番だろう。lib/pureの下に、 80個ほどのファイルがあるけど、どこで使ってる。軽く調査してみる。

[sakae@fedora pure]$ grep template *.nim| cut -d: -f1| sort | uniq -c
      2 actors.nim
      1 algorithm.nim
      5 asyncdispatch.nim
      3 asyncnet.nim
      1 base64.nim
      3 basic2d.nim
      3 basic3d.nim
      6 colors.nim
      2 ftpclient.nim
      1 htmlparser.nim
      6 logging.nim
      1 marshal.nim
      4 memfiles.nim
      4 mimetypes.nim
      4 net.nim
     17 os.nim
      1 osproc.nim
     12 pegs.nim
      1 redis.nim
      7 sockets.nim
      1 strutils.nim
      2 subexes.nim
      1 typetraits.nim
      2 unicode.nim
     16 unittest.nim
      1 uri.nim

ふーん、使い方がまだら模様だなあ。マクロは、ここぞと言う時に定義して、使ってってのが 良い使い方だと思うぞ。

一つだけ定義してる、algorithm.nimあたりを見てみる。ピリリと山椒のように引き立つ 使い方をしてるか?

  template `<-` (a, b: expr) =
    when false:
      a = b
    elif onlySafeCode:
      shallowCopy(a, b)
    else:
      copyMem(addr(a), addr(b), sizeof(T))

marge関数の中に定義されてた。定義名が左矢印っぽいから、copyの方向を間違えないでねって 事を強調する意図があるんだな。

どんな風に使ってるかと言うと、数箇所あったけど、一例、

  when onlySafeCode:
    var bb = 0
    while j <= m:
      b[bb] <- a[j]
      inc(bb)
      inc(j)

コピーの方法は兎も角(マクロに任せて)、右から左へコピーするんだって意志が、強調されてる コードになってるな。

もう一つぐらい見ておく。base64.nim。これを使うと暗号発生・解読器になります。(但し、 素人様限定)

定義は、巨大だった。だって、暗号作成方法がコーディングしてあるんだもん。

template encodeInternal(s: expr, lineLen: int, newLine: string): stmt {.immediate.} =
  ## encodes `s` into base64 representation. After `lineLen` characters, a
  ## `newline` is added.
  var total = ((len(s) + 2) div 3) * 4
  var numLines = (total + lineLen - 1) div lineLen
    :

実際に使ってる現場は、

proc encode*(s: string, lineLen = 75, newLine="\13\10"): string =
  ## encodes `s` into base64 representation. After `lineLen` characters, a
  ## `newline` is added.
  encodeInternal(s, lineLen, newLine)

外部に公開するための薄いラッパーが定義されてて、本体は、マクロを呼んで、展開される んだな。それが(実行の代わりに)コンパイルされるとな。

一箇所だけでこのマクロを使うなら、わざわざ定義する必要は無い。使ってる所は、もう一箇所あるんだ。 そのもう一箇所も、関数名は、上のencode*と同じ。違いは、引数の型。

Nimは、引数の型や個数が違うと、同名の関数名を複数定義出来る。(clojureだと、マルチメソッドとか 言ってたな。)中身が一緒なものは、マクロでくくってしまえって訳。納得した使い方になってますな。

あれ? Lisperがやむに止まれずマクロを使う理由、引数評価タイミングの遅延ってのが あるけど、Nimではどうよ? nimは、コンパイラーだよね。

nimには上記に挙げたtemplateの他に、もっと細かく制御が出来るマクロパッケージとか、構文木に 触れる機能がが付いてますよ。