Haskellにも喋ってもらおう

弾さんのblogを見ていたら夏時間を支持するのは頭が春な人だけ なんて言うコラムが 出ていた。

年に2回も時計を合わせるの、めんどくせー。 時刻を人間の都合で勝手に進めたり 遅らせたりしたら、物理の根底が崩れちゃうよ。それに、時刻は同じものが2度と 現れないと言う性質を使って、唯一性を保証してる某プログラムは発狂しちゃうぞ。 (すんません。面倒なので、TZは無視って事で、プログラムしちゃいました)

海外に居た時、一度だけ、夏時間->通常時間へと変更の現場に立ち会った事がある。(ああ、その瞬間は 寝てました。)朝起きて、食堂へ行ったら、親父さんに、お前の時計は狂ってると、 からかわれた思い出があります。

面倒な事は、ごめんこうむりたい。いくら、adjkerntzがあると言っても、コンピュータ だって、面倒くさいぞ。それに、tzinfoのメンテも必要だし。

[sakae@nil /usr/src/share/zoneinfo]$ lv asia
  .....
# From Paul Eggert (1995-03-06):
# Today's _Asahi Evening News_ (page 4) reports that Japan had
# daylight saving between 1948 and 1951, but ``the system was discontinued
# because the public believed it would lead to longer working hours.''

# Shanks & Pottenger write that DST in Japan during those years was as follows:
# Rule  NAME    FROM    TO      TYPE    IN      ON      AT      SAVE    LETTER/S
Rule    Japan   1948    only    -       May     Sun>=1  2:00    1:00    D
Rule    Japan   1948    1951    -       Sep     Sat>=8  2:00    0       S
Rule    Japan   1949    only    -       Apr     Sun>=1  2:00    1:00    D
Rule    Japan   1950    1951    -       May     Sun>=1  2:00    1:00    D

# Zone  NAME            GMTOFF  RULES   FORMAT  [UNTIL]
Zone    Asia/Tokyo      9:18:59 -       LMT     1887 Dec 31 15:00u
                        9:00    -       JST     1896
                        9:00    -       CJT     1938
                        9:00    Japan   J%sT

Haskellだって喋って欲しいぞ

暫く前に、GHCがメンテナンスリリースされて、ちょびっと版が上がった。いつもなら 直ぐに追従する所だけど、秋口に予定されてるメジャー版アップを思うと、このまま でもいいかなと。

まだHaskell修行中な身ゆえ、Bugを踏むというような事もなかろうかと。で、今まで Rubyやgauche君には喋ってもらっているので、今回是非Haskell君にも喋ってもらいたい なー、と思った次第。

大体、Haskellやってて、悩むのは IOがからんだ部分だけだもの。全開発時間の90% 以上は、IO xxx との、整合性調整に費やしている現状を是非とも打破したいのだ。 で、考えてみたら、喋るをやろうとすると、程よくIOが混じっている事を実感してる ので、練習には、いいかな。

部品の選定

改めて調べてみたら、Windows版には、putenvが無いのね。これは、Unix版との最大の違い かと、愕然としましたよ。嘆いてもしょうがないので、喋るデータを、引数で渡す jsay を使う事にする。これを使うと、データにスペースや<> を含められないと言う、制限 が出ちゃうけど、この際無視する。

次は、mecabとどうやって交信するかだな。データを送り込んで、結果をhaskell側に 戻すと言う難題が控えている。調べてみたら、本物のプログラマはHaskellを使うシリーズで 第14回 Haskellでメッセージ通信を使う利点 が、ヒントになりそうな事が分かった。

あと、もう一つ難題がある。それは、文字コードの変換をどうするかだ。極東の小さな 島国で、大手を振ってしつこく生き延びている、SJISを扱う事が出来るか。utf-8との 間で変換が出来なければ、この計画は頓挫する。

文字コード変換と言えば、そりゃ、nkfでしょと、古くからのunix userは考えてしまう。 Windows版のnkfが有るかと思って調べてみたら、 nkf.exe nkf32.dll Windows用 見つかったよ。手回しよく、Windows 7用動作報告ページまで、作られていて、 つくづくWindowsと付き合っていく大変さを実感した次第。まもなく始まるぞ、阿鼻叫喚な 戦争が!

取り合えず、喋れ

まずは喋る所から。先の本物の... を参考に、jsayとのインターフェースをrawSystemに 任せてみる事にした。だって、なるべく command.comなんて言う時代遅れの骨董品と 付き合う必要なんて無いと思ったからだ。

jsay :: String -> IO ExitCode
jsay s = rawSystem "jsay" [s]

これは、簡単に書けた。gaucheで言う、run-process と考え方は一緒ね。引数は、listに まとめて渡すと。で、書いたのはいいんだけど、どうやって検証するかだな。取り合えず ghci を起動して実験してみよう。

*Main> jsay "12345"
ExitFailure 105

105 って、「音声記号列に未定義の読み記号が指定された」と言うエラーか。ひらがなや カタカナを入れてみても、ghciはutf-8だと思っているので、やはり105しか返ってこない。 諦めムードで

*Main> jsay "<NUMK VAL=1234>"
ExitSuccess

こんな事をしてみたら、喋っちゃったよ。こりゃ、一体どういう事? しばし考えて 合点がいった。commnad.comを経由させなかったので、文字列がそのまま、jsayに届いた のだ。こりゃ、いいわい。

次は、mainを実装してみっか。ファイルを受け取って、それを行に分解し、jsayに 渡せばいいんだ。難しい事は考えずに、向井さんの本から、mapM_ と言うのを 引っ張り出してきた。

main :: IO ()
main = do args <- getArgs
          cs <-  readFile $ head args
          mapM_  jsay $ lines cs

head args ってのは、gaucheで言う所の、(cadr args) だ。これで、数字TAGを並べた ファイルからのデータを読み上げるようになった。

今度は、ひらがなも喋れ

数字(だけですが)喋り始めたので、気をよくして、今度は、ひらがな文も喋れる ようにします。これが完成すれば、選挙戦真っ只中のあの人の代役が務まるかしらん。

command.com嫌いになった私は、runInteractiveProcessを使ってみる事にしました。 GHCのページで使い方を調べるも、いまいちピンとこなかったので、サンプルを 探してみました。

iconvを外部プロセスとして呼び出して漢字コードを変換する ぴったりのものが見つかりました。このページを見たら、rubyに付属してるiconvうん ぬん なんて書いてあったので、自分のマシンにも入っているか調べてみたら、入って いたよ。よって、nkfを入れるのはやめて、そのまま、利用させて頂く事にしました。

結果、出来上がったのは、以下の通り

-- -*- utf-8 -*-
-- speak japanese

import System
import System.IO
import System.Process
import System.Cmd
import List

iconv :: String -> String -> String -> IO String
iconv from to s = do
  (input, output, _, _) <- runInteractiveProcess
                             "iconv" ["-f", from, "-t", to]
                             Nothing Nothing
  hSetBinaryMode input  False
  hSetBinaryMode output False
  hPutStr input s
  hClose input
  hGetContents output

s2u :: String -> IO String
s2u s = iconv "SHIFT_JIS" "UTF-8" s

u2s :: String -> IO String
u2s s = iconv "UTF-8" "SHIFT_JIS" s

jsay :: String -> IO ExitCode
jsay s = rawSystem "jsay" [s]

speak :: String -> IO ExitCode
speak s = do utf <- s2u s
             sjis <- u2s utf
             jsay sjis

main :: IO ()
main = do args <- getArgs
          cs <-  readFile $ head args
          mapM_  speak $ lines cs

テストの為、sjis <--> utf-8 は、ラウンドトリップさせてますが、ここは、おいおい mecab を組み込んだ時に、変更する予定です。