Effective ATS: 単語を数える

この記事では、与えられたファイル中の単語の数を数えるプログラムを紹介したいと思います。 この説明では最終的にそのプログラムに辿り着く過程に注目します。

どんな入力が予想されますか?

基本的に、入力は単語のストリームです。 そこで、次のような型の関数 [word_get] があると仮定してみましょう:
fun word_get (): stropt
[stropt] は、有効な文字列もしくは NULL ポインタであるような optional 文字列型であることに注意してください。 もし [word_get] が NULL ポインタを返したなら、それは与えられた (単語の) ストリーム終端に逹っしたことを示しています。 これは [word_get] が状態を持つ関数であること意味しています。 一般的に、状態を持つ関数を使うことは貧弱なプログラミングスタイルであると考えられています。 例えば、libc の関数 [strtok] は状態がないと間違われるために悪名高いものです。 そしてその被害を受けた誰もが真実を知るのです。 ATS には、単純にそれらをテンプレートに変えることで状態を持つ関数を削除できる便利なアプローチがあります。 差し当り、動く実装を作ることに集中しましょう。

どんな出力が予想されますか?

単語が出現した頻度の高い順番で、出現した全ての単語を出力したいとしましょう。 これは、それぞれの単語とその単語の発生頻度を結びつけるマップを構築する必要があることを意味しています。 そこで、そのようなマップのために次の抽象型 [wcmap_type] と [wcmap_type] の略記として [wcmap] を導入します:
abstype wcmap_type = ptr
typedef wcmap = wcmap_type
[wcmap_type] がボックス化型で、つまりこの型のサイズはポインタの (型 [ptr] の) サイズになることに注意してください。

型 [wcmap] の値はどうやって生成すべきでしょうか? 空のマップを作るために次の関数を導入しましょう:

fun wcmap_create (): wcmap
もし単語が出現したら、その発生回数を 1 増やす必要があります。 これは次の関数で行ないます:
fun wcmap_incby1 (map: wcmap, w: string): void
またそれらの出現回数に従って単語をソートする必要があるので、[wcmap] 値を羅列する関数を導入しましょう:
fun wcmap_listize (map: wcmap): list0 @(string, int)
ATS ライブラリの一部である libats には、多くのマップ実装があります。 データ構造に精通していれば、[wcmap] に最適な構造は hashtable-based map 実装であることは明確でしょう。 もちろん、平衡木 (例: AVL-tree) に基づいたマップ実装も同じく有効でしょう。 私自身の実験では、前者は後者よりも 2-3 倍程度高速でした。

WordCounting を実装する

次のように、単語を数えるためのメイン関数を宣言しましょう:
fun WordCounting (): wcmap
これで [WordCounting] を呼び出すことによって、出現したそれぞれの単語からその出現回数へのマップである [wcmap] を生成できます。 [WordCounting] は次のように実装できます:
implement
WordCounting () = let
//
fun loop
  (map: wcmap): void = let
//
val opt = word_get ()
val issome = stropt_is_some (opt)
//
in
  if issome then let
    val () = wcmap_incby1 (map, stropt_unsome (opt)) in loop (map)
  end else () // end of [if]
end // end of [loop]
//
val map = wcmap_create ()
val ((*void*)) = loop (map)
//
in
  map
end // end of [WordCounting]
本質的に、内部関数 [loop] は [word_get] を呼び出して単語を数えて、それから単語の出現回数を 1 増加させています; [word_get] が ([stropt_is_some] が false を返すような) NULL ポインタを返すと [loop] は終了します。

上記に示した ATS の抽象型を効果的に使ったトップダウンのプログラミングスタイルを、読者が正確に評価できることを、私は期待しています。 私個人の所見では、多くのプログラマはボトムアップスタイルのプログラミングを実用上使っています。 単語を数える問題が与えられたとき、彼等は [wcmap] (とそれに関連した関数群) もしくは [word_get] のような関数を実装することに注目しようとします。 単語を数えるような単純な問題なら、有能なプログラマは彼らが選んだどんなアプローチを用いても、おそらく扱えるでしょう。 けれども、より大きなより複雑な問題を取り扱う時、使われえないコードを書くボトムアップアプローチを用いた彼らは簡単に焦点を失ないうるのです。 ある意味では、プログラムを書くことは物語を語ることと似ています: もし語り部の焦点がはずれると、物語が一貫性を得るのは難しくなります。

どうやって [word_get] を実装すべきですか?

[word_get] を実装する一つの方法は、次の型を持つ関数 [char_get] をはじめに仮定することです:
fun char_get (): int
もし [char_get] が非負の整数を返したら、その整数は ASCII 文字です; そうでなければ、それ以上文字がないことを示しています。

[char_get] に基づいた [word_get] のありうる実装は次のようになります:

implement
word_get () = let
//
typedef
charlst = list0(char)
//
fnx loop
(
// argmentless
) : charlst = let
  val i = char_get ()
in
//
if i >= 0 then
(
  if isalpha (i) then
    loop2 (cons0{char}(int2char0(i), nil0))
  else loop () // end of [if]
) else nil0((*void*))
//
end // end of [loop]

and loop2
(
  res: charlst
) : charlst = let
  val i = char_get ()
in
  if isalpha (i) then
    loop2 (cons0{char}(int2char0(i), res)) else res
  // end of [if]
end // end of [loop2]
//
val cs = loop ()
//
in
//
case+ cs of
| nil0 () => stropt_none ((*void*))
| cons0 _ => stropt_some (string_make_rlist (cs))
//
end // end of [word_get]
[loop2] はアルファベット文字を集めるために呼び出され、[loop] は非アルファベット文字をスキップするために呼び出されることに注意してください。 ([fun] が置かれるべき場所にある) キーワード [fnx] は、[loop] と [loop2] が一緒にコンパイルされ、それら本体の末尾再帰呼出がローカルジャンプに変換できることを意味しています。 関数 [string_make_rlist] は与えられたリストの逆順の文字の並びから成る文字列を生成します。 例えば、'a', 'b', 'c' から成るリストなら、生成される文字列は "cba" です。

どうやって [wcmap] を実装すべきですか?

次のコードは [wcmap] の素直な hashtable-based 実装です。 様々な hashtable 関数の詳細を オンライン で見つけることができます。
local
//
staload
"libats/ML/SATS/basis.sats"
//
staload
HT = "libats/ML/SATS/hashtblref.sats"
//
assume wcmap_type = hashtbl(string, int)
//
in (* in of [local] *)

implement
wcmap_create () =
  $HT.hashtbl_make_nil (i2sz(1024))
// end of [wcmap_create]

implement
wcmap_incby1
  (map, w) = let
//
val opt = $HT.hashtbl_search (map, w)
//
in
//
case+ opt of
| ~Some_vt (n) =>
  {
    val-~Some_vt _ = $HT.hashtbl_insert (map, w, n+1)
  }
| ~None_vt ((*void*)) => $HT.hashtbl_insert_any (map, w, 1)
//
end // end of [wcmap_incby1]

implement
wcmap_listize (map) = $HT.hashtbl_takeout_all (map)

end // end of [local]
上記で示したコード全体を含む実行可能なプログラムは、ファイル wordcnt.dats から入手できます。 またそこには Makefile もあります。

次のリストは、ハーマン・メルヴィルの小説「白鯨」の中で使われている単語の内、使用頻度の高い 100 個を抜き出したものです:

the	->	14515
of	->	6673
and	->	6464
a	->	4799
to	->	4683
in	->	4210
that	->	3080
it	->	2533
his	->	2513
i	->	2127
he	->	1894
but	->	1822
s	->	1816
with	->	1765
as	->	1750
is	->	1748
was	->	1645
for	->	1637
all	->	1535
this	->	1431
at	->	1331
by	->	1211
whale	->	1191
not	->	1169
from	->	1095
so	->	1066
be	->	1062
on	->	1062
him	->	1061
you	->	953
one	->	921
there	->	864
now	->	786
or	->	783
had	->	779
have	->	772
were	->	684
they	->	667
which	->	653
like	->	647
me	->	629
then	->	628
are	->	618
their	->	618
some	->	617
what	->	617
when	->	606
an	->	600
no	->	590
my	->	586
upon	->	566
out	->	537
man	->	527
up	->	523
into	->	522
ship	->	513
more	->	507
ahab	->	501
if	->	500
them	->	471
we	->	470
ye	->	470
sea	->	455
old	->	449
would	->	432
other	->	427
been	->	415
over	->	408
these	->	405
will	->	397
though	->	384
its	->	381
only	->	377
down	->	376
such	->	375
who	->	366
any	->	360
yet	->	345
head	->	344
boat	->	333
time	->	333
her	->	332
long	->	330
captain	->	327
very	->	323
here	->	321
about	->	317
do	->	316
still	->	312
than	->	311
great	->	306
those	->	306
said	->	303
before	->	298
has	->	293
must	->	293
two	->	292
t	->	291
most	->	285
seemed	->	283
当然ですが、whale はこの小説で最もよく使われた名詞で、さらに2番目と3番目はそれぞれ shipsea です。

状態のある関数を状態のないテンプレートに変換する

上記の実装では、関数 [char_get] は状態を持っています。 もし2つのスレッドが同時に [char_get] を呼び出したら、競合状態が発生するかもしれません。 ATS では、テンプレートに変換することで、状態のある関数を取り除くことができます。 例えば、[char_get] を次のように宣言できます:
fun{} char_get (): int
また、[char_get] を直接もしくは間接的に呼び出す関数は、テンプレートとして宣言される必要があります。 これは [word_get] と [WordCounting] も同様にテンプレートに変換する必要があることを意味しています:
fun{} word_get (): stropt
fun{} WordCounting (): wcmap
ここで、次のような関数 [WordCounting_fileref] を宣言してみましょう:
fun WordCounting_fileref (inp: FILEref): wcmap
この関数は、与えられたファイルハンドルから全ての単語を読み出すような [WordCounting] の単なる変形です。 すると [WordCounting_fileref] は次のように実装できます:
local

staload
STDIO = "libc/SATS/stdio.sats"

in (* in of [local] *)

implement
WordCounting_fileref (inp) = let
//
implement
char_get<> () = $STDIO.fgetc0 (inp)
//
in
  WordCounting ()
end // end of [WordCounting_fileref]

end // end of [local]
これで別々のファイルハンドルについて、2つのスレッドが同時に [WordCounting_fileref] を呼び出しても安全です。 上記の [WordCounting_fileref] の実装を含む実行可能なプログラムは、ファイル wordcnt2.dats から入手できます。 またそこには Makefile もあります。

ATS のテンプレートシステムは、コードの組織化と再利用を容易にできる高度なプログラミングの機能です。 実際に使われるテンプレートを解説するために、より多くのプログラミング例を徐々に紹介します。

単語を数えるメモリクリーンな実装

もし全ての動的に確保されたメモリがプログラムが終了する前に直ちに解放されるなら、そのプログラムはメモリクリーン (memory-clean) な実装であると考えられます。 例えば、もしあなたが valgrind を使ってこのプログラムの実行を測定したなら、 その測定結果はいかなるリークも発生する可能性がないことを示すでしょう。

線形型を使うことで、プログラム wordcnt.dats を修正してメモリクリーンな実装にすることができました。 修正されたバージョンの全体は wordcnt_vt.dats から入手できます。 またそこには Makefile もあります。


この記事は Hongwei Xi によって書かれ、 Japan ATS User Group によって翻訳されています。