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

備考

まだ試してないけど、ホスト側のファイルシステムの属性を変えるという対応なのでVagrantVirtualBox)でもそのまま応用できるはず。

ボリュームのサイズはオンデマンドで確保されていくようだ。