POINTの貯め方(Haskell編)

夜散歩してたら、とある家の庭にテントが張られていて、ランタンの灯りを頼りにバーベキューしてた。半仮想、キャンプ場体験ですな。家族の息抜きは必要ですよ。この時期なら、蚊もいないから、いいかも知れないな。

上手な買い物

前々回だったか、文芸的なプログラミングをやった時、haskellで小さなコードを書いた。消費税の計算。単品毎にで税を計算するか? 購入品を合算してから税を計算するかで、端数切捨てルールにより、総支払額が変わってくる。

同じような問題は他にもあるぞ。200円につき1ポイントとか言うサービスね。バラバラに物品を買うより、まとめ買いした方が得ってのは直観的に理解出来る。

じゃ、どの程度の得になるの? こういうのはシュミレーションの出番だな。(同時に、ネタの創出でもあります)

仕様決め

使う言語は、いきがかり上、haskellでいいや。haskellにも乱数を扱うモジュールは有るよね。

でも、haskell内蔵の奴を使うと、haskellにロックインされちゃって、自由度が無くなるな。データは外から与える事にしよう。そうすれば、乱数だけじゃなくて、オイラーが買い物した実データも使えますから。

debianにはhugsが入っている。OpenBSDはGHCだ。両方で動かしたいな。ってか、デフォは占有容量の小さいHugsだな。GHCはどんどん肥大化してるからね。Real World Haskell本をパラパラ見てたら、使ってた6.X代のやつは800Mぐらいだった。それが今では1.6Gですからね。そんな肥大化には付き合いきれません。

パラパラしてたのは、図書館がコロナの影響で閉鎖されちゃったからです。正確には閉鎖ではなくて、ドライブスルー方式に一時的に移行しちゃったから。前もって借りたい本を申請。翌日に取りに行くって事で、接触を減らす。早く通常運転に戻って欲しいぞと。

大雑把な方針として、購入品数は12点。単価はは1000円以下の品。100円で1ポイントになるって事にする。

12点って決めたのは、約数が多いから。1回で12点を購入の場合、12点を2回に分けて購入の場合、以下、3,4,6回、12回って事は、単品購入を12回繰り返す事になる。

優劣は、12点が手に入った時のポイントがどれぐらいだったかと言う事にする。

shellでrandom

pythonとかrubyとか使うのは避けて、どこにも有るshellでしょ。それがUnixerのデフォです。

bashスタイル

ぐぐってみるとbashには乱数を発生する機構が有るそうだ。大きな乱数が得られるので、範囲を制限するには、得られた乱数の余りを求めるようにするとな。基本は下記

echo $(( $RANDOM % 5 ))

そいつを組み合わせて、任意個数の乱数を発生出来る

#! /bin/bash
for i in `seq 12`
do
  echo $(( $RANDOM % 1000 ))
done | tr '\n'  ' '

seqはリナ用、jotはBSD用って事で、汎用性は乏しい。それよりbashってのがガンだな。

odを使って汎用化

bashの魔手から逃れる第二の方法として、/dev/urandomを信号源にする方法。でもこの信号源から得られる乱数はバイナリー。何とか文字列にしたい。そこで登場するのはodと言うコマンド。

od -vAn -tu4 -N4 </dev/urandom

odのオプションの効き具合を、ちょいと確認

ob$ od -N4 < /dev/urandom      ;; N4で4byteを取得
0000000  147754 071645
0000004
ob$ od -vAn -N4 < /dev/urandom    ;; アドレス表示は要らない
         002743 057561
ob$ od -vAn -N4 -tu4 < /dev/urandom ;; 4Byteのデータを符号無し数値で
              3438260501

もっと汎用化

乱数の範囲を制限するのに、bash由来の余り演算を使ってた。これを撲滅したい。更にseqもjotも使わない方式にしたい。そんな事出来るのか?

yesを使えばyesさ。

#! /bin/sh
yes 'echo `od -vAn -N4 -tu4 < /dev/urandom` % 1000 | bc' |
head -n 12 |
sh |
tr '\n'  ' '

yesのオプションで、指定した文字列を永遠に発生する。その文字列は、乱数を発生するコマンド列。bashでの演算を避けてbcを使ってみた。

そのコマンド列をheadで個数制限。shに喰わせて実行。最後は、改行をスペースに置き換えて、一行にまとめている。

haskellerの発想だな。無限文字列発生、takeの代わりにheadを使って必要な個数だけ取り出し。shで評価して、結果をtrで加工。

ob$ echo $SHELL
/bin/ksh
ob$ ./rnd.sh
727 110 279 134 605 938 305 916 403 781 982 855 ob$

これで、OpenBSD環境でも期待通りの結果が得られた。

haskell側のアプリ

ghci and hugsでたたき台作成

まずは、 どんなモジュールが有るかだな。 モジュール にHugsのやつが出てた。 Metaチュートリアル も参考に。そして、こんなのも、 Simple Haskell Example いつも基本的な事で悩むので、ありがたいです。色々な言語用も有るしね。

犬も歩けば棒に当たるで、 Haskellで人工無脳をつくろう こういうのも面白いなあ。

遊んでばかりだとアレなので、今回のたたき台。

-- test code

getInts :: IO [Int]
getInts = do
  cs <- getLine
  return $ map (\s -> read s :: Int) $ words cs

main = do
  xs <- getInts
  print xs
  print $ sum xs `div` 12

Haskell で標準入力(メモ) には、もっと実用的な例が有ったぞ。検索しても、文字列を受け取ってそれを表示するって言う例ばかりしか出てこない。みんな真面目にhaskell使ってるのか?

sakae@pen:/tmp$ ./rnd.sh | runhugs in.hs
[748,976,613,462,425,207,887,696,402,614,48,628]
558
sakae@pen:/tmp$ ./rnd.sh | runhugs in.hs
[330,858,105,43,674,75,412,156,912,536,909,144]
429

ちょいと、結合試験。単価の最大値をもっと下げておいた方が現実的かな。1000円ぽい物品を、ホイホイ購入しませんからね(よーするに、根っからの貧乏人です)。

splitAt

さて、ここからが本番。楽しいH本を見てると、ghciとeditorの間を行ったりきたりしながら、コードを開発してるらしい。hugsの場合も同じ事だな。

使うeditorは勿論emacs。観念してhaskell-modeを入れた。そして、export EDITOR=emacs して、hugsが使うeditorを登録しとく。後は、

sakae@pen:/tmp$ hugs
  :
Type :? for help
Hugs> :l in.hs
Main> main
123 456 789
[123,456,789]
114
Main> :e
Main> :r

 :l で、プログラムをロード。実行してみる。:e で、emacsを起動させて編集。emacsを終了したら、:r で、リロード。後はこれの繰りし。(hugs起動時にプログラムを指定して、ロードを済ませてしまう方がいいかも)

hugsで最初から使える関数群は、Prelude.hsに定義されてる。/usr/lib/hugs/packages/hugsbase/Hugs に置いてあるやつだ。その他、/usr/lib/hugs/packages/base も参考になるかな。つらつら眺めてそれらしい関数に当たりを付ける。

どうやら、splitAtってのが使えそうなんで試してみる。

Hugs> splitAt 3 [1,2,3,4,5,6]
([1,2,3],[4,5,6])
Hugs> splitAt 1 [1,2,3,4,5,6]
([1],[2,3,4,5,6])
Hugs> splitAt 10 [1,2,3,4,5,6]
([1,2,3,4,5,6],[])
Hugs> splitAt 10 []
([],[])

参考までに、splitAtのソースを見ておく。こういう時、Prelude.hsが手元に有ると楽だ。

splitAt               :: Int -> [a] -> ([a], [a])
splitAt n xs | n <= 0 = ([],xs)
splitAt _ []          = ([],[])
splitAt n (x:xs)      = (x:xs',xs'') where (xs',xs'') = splitAt (n-1) xs

こういうプロが書いたコードは何より滋養ですよ。

Haskell API search に行って、調べるのは、ちと億劫ですからね。折角だから対比してみる。(base-4.14)

splitAt n ls
  | n <= 0 = ([], ls)
  | otherwise          = splitAt' n ls
    where
        splitAt' :: Int -> [a] -> ([a], [a])
        splitAt' _  []     = ([], [])
        splitAt' 1  (x:xs) = ([x], xs)
        splitAt' m  (x:xs) = (x:xs', xs'')
          where
            (xs', xs'') = splitAt' (m - 1) xs
splitAt n xs           =  (take n xs, drop n xs)

こういう横着も有りか!!

合体試験

取り合えず、splitAtを使って、mainと結合してみよう。

main = do
  xs <- getInts
  print xs
  print $ map (\n -> calc n xs) [1,2,3,4,6,12]

calc n xs = fst $ splitAt n xs

mainの最後を改変して、計算の要となるcalcを呼び出すようにした。mapに与えているリストは、何個づつ購入計算するかと言う物(12の約数)。

calcは、単に、splitAtして出来るタプルの左値

Main> main
1 2 3 4 5 6 7 8
[1,2,3,4,5,6,7,8]
[[1],[1,2],[1,2,3],[1,2,3,4],[1,2,3,4,5,6],[1,2,3,4,5,6,7,8]]

手動で確認。取り合えず、引数は正しく渡っているな。後は、calcをちゃんとするだけ

calcを極める

大仰なタイトルを付けちゃったけど、splitAtと言うお手本があるので、素直に書けたぞ。

calc _ [] = 0
calc n xs = pc h + calc n t
  where
    (h, t) = splitAt n xs
    pc h   = sum h `div` 100

入力データ(購入品)が無ければ、ポイントは付与しようが有りません(0ポイントを返すのが商人の務め、怒ってはいけません)。次行は、ポイントを加算してる部分。where以下で、細かい計算をして、それを使ってる。勿論、再帰ね。

where以下で、入力データを分割。左項と右項に分けて、hとtって名前を付けておく。次のpc hは、左項のまとまりを合計して、それを100で割って、ポイントの計算。

括弧の中に2つのデータが収まっていると、Lisperはそれを、car,cdrと呼びたくなるのが常。でもね、haskellを使う人は数学屋さんだから、変数名は簡潔に1文字が基本なんだろうね。

この関数の型は明示しなかったけど、hugsが推測してくれている。

Main> :t calc
calc :: Integral a => Int -> [a] -> a

整数と数値のリストを受け取って、数値を返すよ。数値と言うぼかしは、小さい整数(マシン依存)でも大きい整数(いわゆるBig int)でもいいよって事だ。

最終結果

100円で1ポイントって言うのは止めて、AU Payをターゲットに200円で1ポイントにする。 それから、貧乏人モードで、単品の上限は600円未満と言う現実路線を行く事にする。

getInts :: IO [Int]
getInts = do
  cs <- getLine
  return $ map (\s -> read s :: Int) $ words cs

main = do
  xs <- getInts
  print xs
  print $ map (\n -> calc n xs) [1,2,3,4,6,12]

calc _ [] = 0
calc n xs = pc h + calc n t
  where
    (h, t) = splitAt n xs
    pc h   = sum h `div` 200

下記は実行結果。12品の単価リストと、清算方法を変えた場合のポイント計算結果リスト。

sakae@pen:/tmp$ ./rnd.sh | runhugs in.hs
[592,203,14,132,546,518,572,527,581,118,148,174]
[13,17,19,19,20,20]
[154,180,514,109,262,569,116,452,390,247,193,509]
[11,16,16,16,17,18]
[194,261,580,120,440,120,82,164,232,178,270,422]
[9,13,14,14,14,15]

大雑把な結果だと、3品以上をまとめて買うとお得って事だね。

そんなちまちまより、高齢者割引でポイント5倍とかの方がよっぽどいいぞ。 女房はこの日を目指して、一生懸命に買い物リストを作ってる。高齢者 == 婆さん割引だからね。

冒頭にhaskell編と、わざわざ銘打っている。って事は、他の言語でもやるのか?

一つの疑問から

ちゃんと結果も出たし、これで終わりにしても良かったんだけど、一つ疑問が出て来た。

今回定義した、calcは、冒頭がcalcで始まっているのが2行ある。今までlispをやって来た身としては、どうも座りが悪い。何となく2つの同名関数が定義されているよと。 引数の形が違うんで、clojureあたりで自慢してる、マルチメソッドの定義にも、見えるな。

マルチメソッドなら、何処に置こうと、カラスの勝手でしょ。やってみる。calcの最初の行だけを、mainの前に移動してみた。

Hugs> :e
ERROR "in.hs":6 - "calc" multiply defined
in.hs:13:1: error:
    Multiple declarations of ‘calc’
    Declared at: in.hs:6:1
                 in.hs:13:1
   |
13 | calc n xs = pc h + calc n t
   | ^^^^

hugsとghciでは、エラー表示は違えども、二重定義と言っている。lisp頭からhaskell頭に切り替えろ、郷に入っては郷に従えって事だ。

尚、今回はcaclの定義が完全に泣き別れ(間にmainが有る)状態にした結果だけど、間に空行が有る場合は、問題無かった。

でも、一つの関数を定義するのに、何度も同じ関数名が出て来るのは、気にくわん。関数名が衝突して、修正を余儀なくされた時、うっかりミスを誘発しちゃうんじゃなかろうか。

だけども、再帰が基本のhaskellに置いて(他の言語でもそうだけど)、自分自身を呼び出すのが常なんで、避けようが無い。

大体、名前が有るからいけないんだと言う、乱暴な事を考えた人がいた。Yコンビネータの登場である。型、型って五月蝿いhaskellで、コンビネータなんて実現出来るのか? ググレカス。

Haskell で Y コンビネータ

Haskell の Y コンビネータ

不動点コンビネータで無名再帰を作る流れをおさらい

頭が痛くなりそうな事ばかり書いてあるね。五月病じゃないよな。


This year's Index

Home