pureでhistgram

先日、消防署の方から来ましたじゃなくて、消防署から来ましたって、人が訪ねてきた。 あれ、消防署の方なら、消防団の法被を着てるのがデフォだと思うんだけど、怪しい人 なのかしらん? ちゃんと身分証明書を提示してくれたので、まあ安心。

用件は、火災報知機付いてますか? 何処に取り付けておられますか? 今頃そういう 質問ですか? 付いていても、もしもの時に鳴らないんじゃ用を為さないから、点検 させて下さいってのなら、分かるんだけど。。。

その事には何も触れず、ついてますよって答えたら、感心感心って事で、丸を付けて ましたよ。そんなんじゃ、だめじゃん。寒い中、無駄足ですよ。

しょうがない、自主点検しときますかね。電池が切れている、うんぬんより、直接的に タバコの煙でも吹きかけてみればいいのかな? それとも、点検ボタンが付いている?

もう一つ質問があって、何人家族ですか? 65歳以上の方はおられますか? 体の不自由 な方はおられますか? これって、火事の時、何人死ぬか、皮算用してんだな。ああ、 火事だけじゃなくて、地震とかも関連するな。

昔は自主防災って事で、寒い時期の夜間、火のよーじんって言いながら、拍子木を叩いて 近所を回る当番があった。そういうのは、おじーちゃんに押し付けられていて、おいらも 拍子木を叩きたくて、一緒に連れていってもらったな。隣近所、どこの家に寝たきり老人が 居るかなんて、近所の人はみんな承知してたから、わざわざ調べる事は無かったもんね。 現代になって、隣近所の付き合いが希薄になってるんだな。

でも、現代でも拍子木の変わりにサイレンと言うか鐘を鳴らしながら消防車がよく通り かかる。これって、村の消防団が(楽しいから)積極的にやってんだな。おいらも混ぜて おくれよ。

pureでhistgramの下調べ

前回やろうと思っていた、ヒストグラム。これをpureでどう書くか? 引数は当然、分類 したい値が入ったリスト。これをスキャンしながら、どの分類に落ちるか計算し、対応する カウンターをインクリメントする事になる。

でも、pure(当然Haskellも)だと、変数の更新は面倒。カウンター群を配列にしちゃうと、 更にやっかいな事になる。どうする? 惚けた頭では思いつかんな。こういう時はぐぐる先生の 指導を仰ぎましょ。 histogram exersice なんてのを紹介された。悩める人は、世界のあちこちに居るのね。

ベストアンサーはこれだな。

import Data.List
import Control.Monad

-- | Builds a list with (element, multitude) entries, each representing a
--   column in the histogram
histogram :: Ord a => [a] -> [(a, Int)]
histogram = map classify . group . sort
      where classify xs = (head xs, length xs)

main = mapM_ go (histogram list)
      where go (h, l) = putStrLn $ show h ++ " " ++ replicate l '*'
             -- ^ h : head; l: length
            list = [1,2,3,4,2,5,0,0,8,9,1,7,8,9,7,4,7,7,7]

実行してみる。

[sakae@pcbsd ~/src/hs]$ ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Prelude> :l hist.hs
[1 of 1] Compiling Main             ( hist.hs, interpreted )
Ok, modules loaded: Main.
*Main> main
0 **
1 **
2 **
3 *
4 **
5 *
7 *****
8 **
9 **

実行は出来ちゃったけど、コードがチンプンカンプン。宝の持ち腐れ、猫に小判、豚に真珠 ですよ。いきなりHaskellのプロの技を見せられたって盗めないよね。

前の方に、ヒントが書いてあった。

count :: Eq a => a -> [a] -> Int
count x = length . filter (==x)

counts :: Eq a => [a] -> [a] -> [Int]
counts vs xs = map (flip count xs) vs

-- > counts "ab" "abcaaa"
-- [4, 1]

実行例まで付けてくれている。意味を汲み取ると、文字列 abcaaa の中で、aの出現回数と bの出現回数を算出してるんだな。概要は分かったけど、パッと見、コードが分からん。 腰を据えてみてく。

まず、countの方、filterって2引数の関数だよな。2番目の引数を省略してるって、初心者 いじめだな。で、一番目の引数xは期待値か。すると隠れた2番目の引数のリスト(定義を見ると リストって書いてあるじゃん)から、一致するものを引っ張ってくる。そして、その長さを 求めるって事だな。ポイントフリーと関数合成になってるんだな。いちいちカウンターを インクリメントしなくてもいいんだ。発想の転換、王様のアイデア、実用新案xxx号取得済みの ウルトラアイデアですよ。

なんでこいうの思いつかなかったんだろう? 普段、unixでパイプを繋ぎ、分析とかしてるのに。最近だと、smlの所で

cut -f 2 Hist.log | sed -e 's/[0-9].*/dcall/' | sort | uniq -c | sort -nr

こんなのをやった。これ、実行ログから、命令の実行頻度を出すやつ。注目は、並び替えて 同じ命令をカウントするって所が味噌だな。

じゃ、これをpureでも

> cnt x = (# . filter (==x)) ;
<stdin>, line 1: syntax error, unexpected infixr operator '.'

中置演算子だからって言われてもねえ。helpで調べてみる。

f $ g¶
f . g¶

    Like in Haskell, these denote right-associative application and function
    composition. They are also defined as macros so that saturated calls of
    them are eliminated automatically. Examples:

    > foo $ bar 99;
    foo (bar 99)
    > (foo.bar) 99;
    foo (bar 99)

マクロですかい。lispみたいだな。ああ、lispで思い出した。

> (head . tail . tail ) "abcde" ;  // Same as caddr
"c"

それはそうと、何とか、上記のエラーをくぐり抜けられないか。#なんて記号が良くない んだろうと当たりをつけてみる。

> len xs = # xs;
> cnt x = (len . filter (==x)) ;
> cnt "a" "abcaa";
3

旨く当たったな。これってドイツにバグ報告ですかね?

次に定義してあるcountsは、flipで引数の順番を反転させてるんだ。これで分かったよ。

そんじゃ、ベストアンサーに選ばれたコードの解析。肝は、やはりhistgram関数だな。型の 定義を見れば、どんな結果が返ってくるか分かるけど、一度確認しとく。

*Main> histogram  [1,2,3,4,2,5,0,0,8,9,1,7,8,9,7,4,7,7,7]
[(0,2),(1,2),(2,2),(3,1),(4,2),(5,1),(7,5),(8,2),(9,2)]

コードを見ると、sortしたやつをgroupしてる。おいらも前回、自前のgroupなんてのを 勝手に定義したけど、本場のgroupはどう動くの? Data.Listモジュールに定義されてるん だなって事で、ghc wikiの Library documentation を見ると(網羅的に調べるならHoogleの方が便利です)

group :: Eq a => [a] -> [[a]]Source

The group function takes a list and returns a list of lists such that
 the concatenation of the result is equal to the argument. Moreover, 
each sublist in the result contains only equal elements. For example,

 group "Mississippi" = ["M","i","ss","i","ss","i","pp","i"]

It is a special case of groupBy, which allows the programmer to supply 
their own equality test.

折角なので、コードはどうなってるか、 hackage を引いてみると、

group                   :: Eq a => [a] -> [[a]]
group                   =  groupBy (==)

-- | The 'groupBy' function is the non-overloaded version of 'group'.
groupBy                 :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy _  []           =  []
groupBy eq (x:xs)       =  (x:ys) : groupBy eq zs
                           where (ys,zs) = span (eq x) xs

ソース内に、上記のドキュメントも書かれていた。hackageを引くのが一挙両得だな。 さて、そんじゃpure流のhistgramを実装するかな。目標は、前回やったgplotで表示出来る 事にしよう。データを幾つに分類するかは、悩ましい問題。

大体10ぐらいでいいだろうって言うアバウト派もあれば、母数を元に計算させる派も あるみたい。おいらは見栄え重視派で、一番それっぽく見えるように外部から制御させる 事にしよう。制御って言うと難しそうに聞こえるけど、分割の幅を与える事にする。どの数値から 分類を始めるかは、データの最小値からって事でいいだろう。

冬なのに虫が湧いたぞ

そんな良い加減の仕様で書いたコードが、下記。

hist s xs = mk n ss with
              ss = sort (<) xs;
              n = s + minimam xs ;
              cs n xs = (n - s, (head $ takewhile (<n) xs));
              mk n [] = [] ;
              mk n ss = cs n ss : mk (n+s) (dropwhile (<n) ss) ;
            end ;

これを走らせてみます。

> hist 3 (1..5) ;
<stdin>, line 1: unhandled exception 'failed_cond' while evaluating 'hist 3 (1..5)'
> bt
bt: debugging not enabled (try run -g)

なにやらエラーですよ。制御できないものだとさ。どこまで走ったかみようとしたら、そりゃ デバッガーの領分だと。emacsから、C-c C-k で起動してると、

/usr/local/bin/pure -q -i --noediting hist.pure

ってな具合に動いて不便。pure-mode.elを修正しておこう。

再起動して、再度走らせてからbtすると

<stdin>, line 2: unhandled exception 'failed_cond' while evaluating 'hist 3 (1..5)'
> bt
   :
>> [6] #<case>: x:xs = tick (x:zs) xs if p x;
     p = flip (<) (3+minimam [1,2,3,4,5]); x = 1; xs = [1,2,3,4,5]; zs = []

どうやら、minimanってのが解決出来ないっぽい。pureには無いのか。しょうがないので

              n = s + foldl1 min xs ;

こんな風に、minimamの代わりを務めさせた。これで試すと

> hist 3 (10..20);
[(10,10),(13,13),(16,16),(19,19)]

おかしいなあ、虫取り器の出番かな。それもいいんだけど、今回はtraceを試してみよう。 schemeっぽくね。ghciにはデバッガーが有る みたいだけど、トレーサーは有るのかな?

[sakae@manjaro pure]$ pure -v -g -i hist.pure | tee LOG
> trace hist
> hist 3 (10..20) ;
  :
^D

出てきたメッセージをそのまま見てもいいんだけど、前後に辿ったりサーチしたりが大変 なので、teeを使ってログに落としておきました。後はログを見るだけね。

++ [3] cs: cs n xs = n-s,head (takewhile (flip (<) n) xs);
     n = 13; s = 3; xs = [10,11,12,13,14,15,16,17,18,19...
     --> 10,10
  :
++ [4] cs: cs n xs = n-s,head (takewhile (flip (<) n) xs);
     n = 16; s = 3; xs = [13,14,15,16,17,18,19,20]
     --> 13,13

プログラムは思った通りじゃ無く、書いた通りに動く。headなんて使たって思った通りには ならんわな。思い込みと言うかhaskellの例をそのままコピペしちゃったんだな。ああ、恥ずかしい!

でも、ちょいと面白い事に気付いた。pure内部では、(効率の為か)コードを並べ変え(展開も)て いるね。out of order って、インテルだけかと思っていたぞ。どんな風に並べ変えて いたかと言うと

hist s xs = mk n ss with 
   n = s+foldl1 min xs; 
   mk n [] = [];
   mk n ss = cs n ss:mk (n+s) (dropwhile (flip (<) n) ss);
   ss = sort (<) xs;
   cs n xs = n-s,head (takewhile (flip (<) n) xs)
 end;

最終的に出来上がったのは、下記。

hist s xs = mk n (sort (<) xs) with
              n = s + foldl1 min xs ;
              len xs = # xs ;
              cs n xs = (n - s, (len $ takewhile (<n) xs));
              mk n [] = [] ;
              mk n ss = cs n ss : mk (n+s) (dropwhile (<n) ss) ;
            end ;

前回の血圧・脈拍データから、脈のヒストグラムを、色々なステップ数でサンプリングしてみた。

> hist 5 pls;
[(52,51),(57,247),(62,57),(67,5)]
> hist 4 pls;
[(52,23),(56,162),(60,134),(64,40),(68,1)]
> hist 3 pls;
[(52,16),(55,67),(58,201),(61,35),(64,36),(67,5)]
> hist 2 pls;
[(52,5),(54,18),(56,60),(58,102),(60,113),(62,21),(64,31),(66,9),(68,1)]
> hist 1 pls;
[(52,2),(53,3),(54,11),(55,7),(56,28),(57,32),(58,43),(59,59),(60,99),(61,14),(62,21),(63,0),(64,20),(65,11),(66,5),(67,4),(68,0),(69,1)]

グラフを書く場合、最高血圧なら

gplot::plot gp (map (!1) $ hist 4 hi) ("style", "histogram", "fill solid");

これぐらいが、富士山風シルエットで綺麗だった。分類数で10になってたから、ばらつきが非常に大きいのね。 それから、pureにはタプルの演算、fstとかが定義されていなくて、スロット番号で アクセスする。ドイツ方言、健在なり。

ag

上で、ヒストグラムのグラフを書いたけど、表示は値だけだ。X軸の目盛りと言うか ランクは反映されていない。調べればちゃんと反映出来るんだろうけど、めんどう。

たまには、CUIで星印を重ねたグラフも良かろう。アスキーアート(AA)ならぬ、アスキーグラフ(AG)って 事で、agってのを定義してみるか。(決して、広告機構の回し者ではありません)

バーが延びても、1行80文字以内には抑えたいな。そうすると、星印は50個ぐらいまで だな。データ値によってスケーリングが必要か。長くなりそうなので、次回に回そう。

こういうのは、ネタの温存ですな。まあ、いいか。

zip系

上で出てきたhistの結果って、zip関数の戻り値と同じ形をしてるね。unixにzipコマンドと unzipコマンドが有るぐらいだから、haskellにも有って当然だよね。

zipの引数は2つのリスト。ならば、その逆演算であるunzipの結果は、2つのリストに なって当然と思う。のだけど、haskellは多値が嫌いらしく、タプルに包んで、見掛け上 一つの値として返ってくる。(事になっている。大人の都合って訳だな。)

> hist 5 pls;
[(52,51),(57,247),(62,57),(67,5)]
> unzip $ hist 5 pls;
[52,57,62,67],[51,247,57,5]

リスト間を区切るのに、微妙にカンマが使われていて、タプルだよーーってのを、とことん 隠す工夫がされていた。

> let a = 1, 2, 3, 4;
> a;
(1,2,3,4)

うぬぬ、コンテキストによって、見かけが違うぞ。後で、こういうのに嵌まるんだよなあ。 PythonのPEPが言うように、タプルはそれとはっきり分かるように、括弧で包みましょうって のが、おいらは好きだな。どうせ、Schemeみたいに多値の仕組みは無いんだしさ。

> let b = 1, ;
<stdin>, line 16: syntax error, expected operand after infix operator ','
> let b = (1,);
> b;
(,) 1
> let c = (1,2);
> c;
1,2
> let d = (1,2,3,4);
> d;
1,2,3,4

紛らわしいなあ。要注意人物ですだ。ああ、zipが出てきたのでzip系の優れ者を みとくと

> let a = (1..5);
> let b = (10..15);
> zipwith (+) a b;
[11,13,15,17,19]

これ、便利そう。使える場面が有ったら、忘れずに使ってみよう。これもhaskell道の 修行のうち。

気になるシャープ

関西方面のじゃなくて、pure内で使われているやつね。気になったので探ってみた。

prefix  2600   # ;                 // size operator

#()             = 0;
#(x,xs)         = accum 1 xs with
  accum n::int (x,xs)   = accum (n+1) xs;
  accum n::int x        = n+1;
end;

#[]             = 0;
#(x:xs)         = accum 1 xs with
  accum n::int (x:xs)   = accum (n+1) xs;
  accum n::int []       = n;
  accum n::int xs       = n+#xs;
end;

定義では、接頭語だなあ。タプルとリストに対して関数が定義されてる。問題を起したのは 関数合成の所だった。

def f $ x       = f x;
def (f . g) x   = f (g x);

前に見た説明書の通り、マクロとして定義されてた。

> (# . tail) "abcd" ;
<stdin>, line 2: syntax error, unexpected infixr operator '.'

エラーになるな。でも、defの定義に則り、手動展開してから実行すると

> # (tail "abcd");
3

成功した。prelude.pureの結合則を適用すれば

> # . ;
<stdin>, line 3: syntax error, unexpected infixr operator '.'

見事に同じエラー。ならば、prefixの定義を末梢すればいいじゃんと思ってやってみた。 そしたら、これに頼ってる所があちこちにあって、まともに動かなくなってしまった。

悪あがきはやめて、ここは素直に作者様(神)の意思に従いましょ。