Haskellでpaiza
すごいH本でいうと11章まできたが、先に進むより現時点の知識でHaskellコードたくさん書いた方がいいと思ったのでpaizaでもやってみる。ここでは繰り返し現れる入力データの処理についてまとめる(閏年の練習問題でいきなり躓いたのだ…)。
基本
do構文でゴリっていくのはHaskell使ってる意義が薄れるため、入力文字列→出力文字列の変換を行う関数を定義する形で解いていきたい。まず、入力をそのまま結果として出力する場合は:
main = interact id
同じechoの動作だけど、行単位の[String]
に分解してから改行でjoinしてString
に戻して出力する場合は:
import Data.List main = interact (unlines . lines)
頻出パターンのパース
paizaの入力データは、データ数→各データが改行区切りで渡されるパターンが多い。例えば最初に4組の2次元座標、次に3個の単語が入力として渡されるならこんな感じ:
4 10 20 45 60 2 3 76 8 3 red yellow blue
この入力データを、手始めとして次の形に整形したい:
[["10 20", "45 60", "2 3", "76 8"], ["red", "yellow", "blue"]]
各行についてさらにスペースで分割したり、整数に変換したりは問題ごとの要件となる。
カウンタを更新しながら順次データを取り込む方法にちょっと悩んだが、タプルをアキュムレーターとして畳み込むことでうまくいった。また、リストは先頭に追加する方が簡単なので、逆順でリスト化してからreverseする方法をとった。
import Data.List type DataSet = [String] -- 思考整理のために型シノニムを積極的に利用 parsePaizaLine :: (Int, [DataSet]) -> String -> (Int, [DataSet]) parsePaizaLine (0, ys) x = (read x, []:ys) -- データ数の行なので、カウンタを設定して新たなリストを用意する parsePaizaLine (n, y:ys) x = (n - 1, (x:y):ys) -- データ行はカウンタを減らしつつリストの末尾に取り込んでいく parsePaizaInput :: String -> [DataSet] parsePaizaInput = reverse . map reverse . snd . foldl parsePaizaLine (0, []) . lines main = interact (unlines . solve . parsePaizaInput)
雛形はこんな感じで、問題解決の本体としてsolve
関数を定義する。
例題
与えられた数字が7ならばLUCKY SEVEN!!
、7以外ならNOT LUCKY...
を出力せよ。
入力例(最初の行は与えられるデータ数を表す)
5 456 7 2 610 7
入力例に対する期待出力
NOT LUCKY... LUCKY SEVEN!! NOT LUCKY... NOT LUCKY... LUCKY SEVEN!!
回答例
import Data.List type DataSet = [String] parsePaizaLine :: (Int, [DataSet]) -> String -> (Int, [DataSet]) parsePaizaLine (0, ys) x = (read x, []:ys) parsePaizaLine (n, y:ys) x = (n - 1, (x:y):ys) parsePaizaInput :: String -> [DataSet] parsePaizaInput = reverse . map reverse . snd . foldl parsePaizaLine (0, []) . lines sayLucky :: Int -> String sayLucky 7 = "LUCKY SEVEN!!" sayLucky _ = "NOT LUCKY..." solve :: [DataSet] -> [String] solve = map (sayLucky . read) . head main = interact (unlines . solve . parsePaizaInput)
こんなノリでいいのかなHaskell?
おまけ
generatePaizaInput :: Int -> String generatePaizaInput n = unlines . map show $ n : take n [1..] main = print . length . head . parsePaizaInput $ generatePaizaInput 1000000
パース関数が大量データに耐えうるか実験してみた。2000000ではタイムアウト。この場合検証用データの生成にも時間かかってるので、パース関数のパフォーマンスについてはひとまず大丈夫そうか。
目標は「嘘つき探し」の攻略ということで。
Docker for Macのファイル共有がめちゃくちゃ遅い
ホストと共有したディレクトリでcrosstool-ngのビルドなど行ってみると信じられないほど時間がかかったので簡単に検証してみる。
検証方法
10000個のファイルの作成・削除にかかる時間を、コンテナ内ディレクトリと共有ディレクトリについて計測する。 ファイル作成用のスクリプトは以下:
root@e42095502d60:/# cat /usr/bin/mkfiles #!/bin/bash mkdir tmp for i in $(seq $1); do echo $i >>tmp/$i.txt done
結果
まずコンテナ内ディレクトリ
root@e42095502d60:~# pwd /root root@e42095502d60:~# time mkfiles 10000 real 0m0.297s user 0m0.070s sys 0m0.220s root@e42095502d60:~# time rm -r tmp real 0m0.167s user 0m0.000s sys 0m0.160s
次に共有ディレクトリ(-v ${PWD}/synctest:/synctest
でコンテナを起動している)
root@e42095502d60:/synctest# pwd /synctest root@e42095502d60:/synctest# time mkfiles 10000 real 0m15.094s user 0m0.460s sys 0m1.520s root@e42095502d60:/synctest# time rm -r tmp real 0m5.633s user 0m0.030s sys 0m0.330s
冗談かと思うくらい遅い。
結論
大量のファイルを扱う操作はコンテナ内で閉じるようにして、必要最小限のファイルだけ共有ディレクトリに置く。
Macでファイル名の大文字小文字を区別する(High Sierra以降)
仮想Linux環境とファイル共有するとき、ホストであるMac側のファイルシステムがケースセンシティブじゃないのでたまに困る。High Sierra以降だと簡単に対応できるので以下に手順をまとめる。
APFS
High Sierra から新しいファイルシステムAPFS(Apple File System)が導入されている。 APFSでフォーマットしたパーティションはAPFSコンテナとして扱われ、コンテナ内には任意の数のボリュームを作成することができる。そして、ボリュームごとに違うフォーマットにすることができる。
つまりAPFS(大文字/小文字を区別)でボリュームを追加するだけで問題解決する。
手順
- ディスクユーティリティを起動
- 左のリストからMacintosh HDを選択
- パーティション作成をクリック
- ボリュームを追加をクリック
- 任意の名前を設定し、フォーマットをAPFS(大文字/小文字を区別)にして追加を押す
追加したボリュームは設定した名前で/Volumesにマウントされている。以下は"dev"という名前で追加した例。
$ ls -l /Volumes total 0 lrwxr-xr-x 1 root wheel 1 10 9 11:07 Macintosh HD -> / drwxrwxr-x 6 root admin 192 10 14 17:52 dev
効果確認
追加したボリューム内のディレクトリをDockerコンテナにマウントして、ケースセンシティブな操作をしてみる。
$ docker container run --rm -it -v /Volumes/dev/hoge:/docker/hoge busybox /bin/sh / # cd /docker/hoge /docker/hoge # touch fuga /docker/hoge # touch FuGa /docker/hoge # ls FuGa fuga /docker/hoge # exit $ ls /Volumes/dev/hoge FuGa fuga
Macに戻ってから確認しても、fugaとFuGaが共存している。
デフォルトのボリューム内のディレクトリをマウントして同じ操作を行うと、以下のようにLinuxの時点で両者を区別できない。
$ cd $ docker container run --rm -it -v ${PWD}/hoge:/docker/hoge busybox /bin/sh / # cd /docker/hoge /docker/hoge # touch fuga /docker/hoge # touch FuGa /docker/hoge # ls fuga /docker/hoge # exit $ ls hoge fuga
備考
まだ試してないけど、ホスト側のファイルシステムの属性を変えるという対応なのでVagrant(VirtualBox)でもそのまま応用できるはず。
ボリュームのサイズはオンデマンドで確保されていくようだ。