ghcjsをhardened profileのGentooにインストールするには

今年もよろしくおねがいします.


大晦日にghcjsのREADMEが更新され,ghc-7.6ベースになったり,これまで煩雑だったインストール作業がスクリプト化されたりするなど,いろいろと変更があった.実にはタイミングの悪いことに,このREADME更新が行われる直前,半年振り?くらいにghcjsをビルドしてみようか*1と作業し初めてしまっており,何かインストール手順に記載されてないパッケージに依存するようになってるなぁとかで試行錯誤したりしてイロイロとハマっていたのだが,なんのことはない,ちょっと大きな変更の過渡状態で作業していたと知ってガックリ.

で,例によって?hardened profileなGentooだとインストーラスクリプトそのまま走らせてもビルドエラーで入らないので,どうしたかについて備忘録として記載しておく.ちなみに実験環境はVirtualBox上のGentoo amd64 hardened.ちなみにdefault profileについてはインストール試していないが,もしそっちで何かダメでも同様の方法で動かせるようにはなるはずだろう.

基本はPIE/SSPビルドを無効にするにはどうすればよいですか?の項にある作業をghcjs公式のインストール手順・インストーラスクリプトに挿し込むことになる.ちなみにこの作業はghcをemergeする際にも行われている.ルートになって下記手順でghcビルド時のオプションがどうなるか確認できる.

ebuild /var/lib/layman/haskell/dev-lang/ghc/ghc-7.6.1.ebuild configure
cat /var/tmp/portage/dev-lang/ghc-7.6.1/work/ghc-7.6.1/mk/build.mk

build.mkの内容はたとえばこんなカンジになっている.

# Gentoo changes
docdir = /usr/share/doc/ghc-7.6.1
htmldir = /usr/share/doc/ghc-7.6.1
SRC_HC_OPTS+= -optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack
SRC_CC_OPTS+=-march=native -pipe -O2 -march=native -march=native -nopie -fno-stack-protector -Wa,--noexecstack -Wa,--noexecstack
SRC_LD_OPTS+= -nopie
BUILD_DOCBOOK_PDF  = NO
BUILD_DOCBOOK_PS   = NO
BUILD_DOCBOOK_HTML = NO
HADDOCK_DOCS       = NO
SRC_HC_OPTS+=-w

基本この条件をそのままghcjsビルド時にも与えてあげればよい.が,一般ユーザのHOME以下に一般ユーザの権限でビルドして入れるので,docdir/htmldirの指定はこのままだとマズい.消してしまおう.

また,SRC_HC_OPTSはインストーラスクリプトghcjs-build.sh内からcabalが呼ばれるときにも--ghc-optionsとして与えてあげる必要がある.(これは設定次第で不要になるんじゃないかなとも思っている*2のだが,追い切れていない.だれか知ってる人情報キボン)

cd
git clone https://github.com/ghcjs/ghc
cd ghc
git checkout ghc-7.6
./sync-all -r https://github.com/ghc get
./sync-all -r https://github.com/ghcjs --ghcjs get
./sync-all checkout ghc-7.6
cabal update
./unpack.sh
cat > mk/build.mk <<EOF
SRC_HC_OPTS+= -optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack
SRC_CC_OPTS+=-march=native -pipe -O2 -march=native -march=native -nopie -fno-stack-protector -Wa,--noexecstack -Wa,--noexecstack
SRC_LD_OPTS+= -nopie
BUILD_DOCBOOK_PDF  = NO
BUILD_DOCBOOK_PS   = NO
BUILD_DOCBOOK_HTML = NO
HADDOCK_DOCS       = NO
SRC_HC_OPTS+=-w
EOF
nano -w ghcjs-build.sh # cabal/cabal-meta に --ghc-options="SRC_HC_OPTSの内容"を追加する.
export PATH=$HOME/.cabal/bin:$PATH # ghcjs-build.shはこのパスが通っていることを前提としてるっぽい.
./ghcjs-build.sh

でもってこのインストールスクリプト,結構時間がかかるため,ある程度覚悟を決めるか寝るか別の作業をしよう.個人的には寝るのをお勧めする.仮想マシン環境とはいえ今回の実験環境は現時点ではそれなりにパワーのあるマシン(Intel(R) Core(TM) i7-3960X CPU @ 3.30GHzで仮想環境として割り振りはCPU8コア&メモリ8GB)と言えるほうだが,それでも以下のように1.5hかかっている.

./install-ghcjs.sh  9672.08s user 3072.23s system 234% cpu 1:30:29.01 total

通常何もしなければghcjsは使われないが,

export PATH=$HOME/ghcjs/bin:$PATH

するとghcjsが有効な状態になる.

一応これまででghcjsインストール完了ではあるのだが,webkitなども入らないことには十全にghcjsを使うことができないので,exampleまで入れる.というかそこにも罠があるのでそっちまで解説しなければ片手落ちだったりする.今回は事前にwebkit-gtk-1.8.3-r200をemergeしている.gtk+-2.24.12なのでcabal-meta install時に-fgtk3は無し,また,webkit-1.10系じゃないので-fwebkit1.8を付けている.

export PATH=$HOME/ghcjs/bin:$PATH
git clone https://github.com/ghcjs/ghcjs-examples.git
cd ghcjs-examples
cabal-meta install --force-reinstalls -fwebkit1-8 --ghc-options="-optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack"

とやっても,残念なことに失敗しているハズだ.これは gtk系hackage特有の問題?に遭遇している.gtk系hackageは通常のsetupの他にsetup-wrapperというものを作ってビルドを行うようになっているのだが,cabal installで--ghc-optionsに指定した内容が奥のほうまで届かないようなのだ.バグ臭い.

なので,一度周辺をクリアして, --ghc-options を強制的に挿し込むんでもらうようにコードを弄った上でリトライする.

rm -rf vendor/*/dist vendor/gtk2hs/*/dist
sed -i 's/, setupDir]/, setupDir, "-optc-march=native", "-opta-march=native", "-optc-nopie", "-optl-nopie", "-optc-fno-stack-protector", "-optc-Wa,--noexecstack", "-opta-Wa,--noexecstack"]/' vendor/gtk2hs/*/SetupWrapper.hs vendor/webkit/SetupWrapper.hs vendor/webkit-javascriptcore/SetupWrapper.hs
cabal-meta install --force-reinstalls -fwebkit1-8 --ghc-options="-optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack"

途中で

gtk2hsC2hs: UName: root name supply used after saving

が原因のエラーになることがあるが,気にせずもう一度叩くとキモいけど通るようになるとのことなのでそうすると確かにキモいけど何故か通る.

ghcjs-min ~/.cabal/bin/ghcjs-hello

で,~/.cabal/bin/ghcjs-hello.trampoline.jsexe 以下にJavaScriptが吐かれているのが確認できる.あとはコイツをブラウザから見える状態にしてやればよい.

ghcjs有効状態にしてcabal installでパッケージを入れると,各モジュール毎に対応するJavaScriptもモリモリ吐き出していく様は圧巻. cabal install --helpを見ると--enable/disable-java-scriptなどというオプションがあるのもおもしろい.

ghcjsはまだ発展途上だが,fay等と比べ,

  • 既存のパッケージが通常通りcabal installして使える
  • webkitによるStandalone BinaryでWebサーバに置かずとも動作を確認できる

など優れた点が多い,逆に,

  • FFIが弱い(=既存のJavaScriptライブラリは使いにくい)

など力弱い点もあるが,むしろunsafeで名状し難いJavaScript跳梁跋扈する深淵の覗き窓*3を作るくらいなら弱いままでも良いくらいという向きもあるだろう.とにかく,ghcjsの今後ますますのご発展を心よりお祈り申し上げます.

*1:だいたい[http://blog.konn-san.com/article/20121225/fay-introduction:title=こんさんの記事]のせい

*2:通常のcabalコマンドがこの指定を必要としていないので

*3:ああ!窓に!窓に!

fluent-logger-haskell作った

なんかイマサラだけど,少し前にfluent-loggerのHaskell版と,そのconduitインターフェース版を作った.

Haskellからfluentdにポンポンとイベントログ吐ける.

以下,process-conduitとcsv-conduitでdstatの出力を吐き出す*1例,

{-# LANGUAGE OverloadedStrings #-}
import Prelude hiding ( null )
import qualified Data.HashMap.Strict as HM
import Data.Conduit
import Data.Conduit.Process
import qualified Data.Conduit.List as CL
import Data.CSV.Conduit
import Network.Fluent.Logger
import Network.Fluent.Conduit
import Data.ByteString ( ByteString, null )
import Data.ByteString.Char8 ( unpack )
import Data.List ( groupBy )
import Data.Function

type Formatter a = [ByteString] -> [ByteString] -> [ByteString] -> a

settings :: FluentSettings
settings = defaultFluentSettings { fluentSettingsTag = "dstat"
                                 , fluentSettingsHost = "127.0.0.1"
                                 }

main :: IO ()
main = runResourceT $
       sourceDstat "/usr/bin/dstat" ["-cm"] =$=
       awaitForever (mapM_ yield) $$
       sinkFluent settings

sourceDstat :: MonadResource m =>
               FilePath
            -> [String]
            -> GSource m [ ( ByteString, HM.HashMap ByteString Double) ]
sourceDstat dstat opts = run cmd >+> toCSV >+> convert toDstatMap
    where
      run = sourceCmd . unwords
      cmd = dstat : opts ++ [ "--output", "/dev/fd/3"
                            , "3>&1", ">/dev/null", "2>/dev/null" ]

toCSV :: MonadResource m =>
         GInfConduit ByteString m [ByteString]
toCSV = injectLeftovers $ intoCSV defCSVSettings

convert :: Monad m =>
           Formatter o
        -> Pipe l [ByteString] o u m ()
convert format = CL.drop 5 >> withAwait (withAwait . convert' . fill)
    where
      convert' headers subs = withAwait $ \xs -> do
                                yield $ format headers subs xs
                                convert' headers subs

withAwait :: Monad m =>
             (i -> Pipe l i o u m ())
          -> Pipe l i o u m ()
withAwait f = await >>= maybe (return ()) f

toDstatMap :: Formatter [ (ByteString, HM.HashMap ByteString Double) ]
toDstatMap headers subs xs =
    [ ( fst $ head zs
      , HM.fromList $ map snd zs )
    | let ys = zip headers $ zip subs $ map (read . unpack) xs
    , zs <- groupBy ((==) `on` fst) ys
    ]

fill :: [ByteString] -> [ByteString]
fill = scanl1 (\x y -> if null y then x else y)

fluentd側で標準出力に出してみるとこんなカンジ,

...
2012-12-29 17:33:49 +0900: adding match pattern="dstat.**" type="stdout"
2012-12-29 17:33:49 +0900: listening fluent socket on 0.0.0.0:24224
2012-12-29 17:33:49 +0900: listening dRuby uri="druby://0.0.0.0:24230" object="Engine"
2012-12-29 17:34:00 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.005,"siq":0.003,"sys":0.099,"usr":0.169,"idl":99.723}
2012-12-29 17:34:00 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707342336.0,"free":3712630784.0}
2012-12-29 17:34:01 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.125,"idl":99.875}
2012-12-29 17:34:01 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707350528.0,"free":3712622592.0}
2012-12-29 17:34:02 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.0,"idl":100.0}
2012-12-29 17:34:02 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:03 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.124,"usr":0.0,"idl":99.876}
2012-12-29 17:34:03 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:04 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.126,"siq":0.126,"sys":0.0,"usr":0.0,"idl":99.748}
2012-12-29 17:34:04 +0900 dstat.memory usage: {"buff":349929472.0,"cach":3611160576.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:05 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.125,"idl":99.875}
2012-12-29 17:34:05 +0900 dstat.memory usage: {"buff":349929472.0,"cach":3611160576.0,"used":707604480.0,"free":3712368640.0}

動確取ってるのは現在のhaskell-platformに積んであるghc-7.4だが,今現在hackageはghc-7.6でビルドしようとするので,依存してるmsgpackがTHガラミか何かでBuild failureになってしまう.そういうときドキュメントが生成してくれないのがhackageのちょっと悲しいトコロ.

*1:それfluent-plugin-dstatで(ry

Hackage2によるインハウスパッケージサーバを試す

この記事は Haskell Advent Calendar 2012 15日目の記事です.


みんな何かヨサゲなもの作ったら hackage に upload してシアワセおすそわけしてると思う.ああ,すばらしきこのせかい.ユウジョウ!

が,たとえば,オシゴトとかでソースをオープンにせずに Haskell で何か作りたい,けど cabal install で開発者がみんなそれぞれ利用できる状態にしておきたい…って状況出てくるんじゃないかなーと思う.こんな世知辛〜いケースでは当然 hackage を使うわけにはいかない.ヤンナルネ.どうにかしてあげなければならない.具体的には maven2 の inhouse repository のようなもの,つまり inhouse hackage server が必要だ.

yackageこのエントリ にもある通りそんな役割を持っている.しかし, yackage は開発中のものを hackage への upload した後の状況をテストするために必要な機能しか持っていない.例えば,ユーザアカウントや,それに伴う権限管理が無かったりする.

なので,試しに hackage2 を使ってみよう.公式にビルド方法が書いてあるが,実際にやってみるとエラー踏むことがあったので,その解決も含め立ち上げ作業は以下のような感じになった.ちなみに私は余程コアなパッケージ以外には cabal-dev を使う.(コアなパッケージはportageで)

$ darcs get http://code.haskell.org/hackage-server/
$ cd hackage-server
$ cabal-dev install --only-dependencies
$ cabal-dev configure -O0
cabal: The program alex version ==2.2.* || ==2.3.* is required but the version
found at /usr/bin/alex is version 3.0.2
$ cabal-dev install alex-2.3.5
dist/build/alex/alex-tmp/Scan.hs:344:17:
    Illegal bang-pattern (use -XBangPatterns):
    ! (base)
Failed to install alex-2.3.5
cabal: Error: some packages failed to install:
alex-2.3.5 failed during the building phase. The exception was:
ExitFailure 1
$ cabal-dev install alex-2.3.5 --ghc-options=-XBangPatterns
$ export PATH=$(pwd)/cabal-dev/bin:$PATH
$ cabal-dev configure -O0 --constraint='alex ==2.3.5'
$ cabal-dev build
$ ./dist/build/hackage-server/hackage-server init --static-dir=static/ --admin=notogawa:********
$ ./dist/build/hackage-server/hackage-server run --static-dir=static/

さて,これで準備は整った.デフォルトだと http://localhost:8080/ でアクセスできる.

hackage2 ではユーザアカウントがあり,さらに各アカウントに対しhackage2に対する操作権限管理が設定できる. http://localhost:8080/admin.html から uploader のリンクを辿ると,どのユーザがこのhackage2に対するアップロード権限を持ってるかをコントロールできる.また,一度 upload したパッケージに対しては,パッケージ毎に maintainer を設定することができる.リリースマネジメント担当者など登録しておくのがよいだろう.

hackage のミラーなどもできるようだ.今回は目的外なので試してはいない.

各ユーザがこのローカルに立てた hackage2 を参照するには, cabal config ファイルに以下のように remote-repo を追加して,

 remote-repo: hackage.haskell.org:http://hackage.haskell.org/packages/archive
+remote-repo: localhost:http://localhost:8080/packages/archive
 remote-repo-cache: /home/user/.cabal/packages

update すればよい,

$ cabal update

これでもうローカルに upload したパッケージを cabal install できるようになっている.

さて,この状態でチョイと気になるのは cabal upload したらどうなるの?というところではないだろうか? config に remote-repo が2つ書かれているところから,公式に上がるのか,それともローカルに上がるのか….オープンにしない目的でローカルを立てたのにウッカリ公式のほうに上がってしまったりしたらインシデントである.

で,いきなり結果だが.どうも現状 cabal upload のターゲットは後に書いた方のremote-repoのようだ.しかも, hackage2 のアップロード用URLにささっておらず,いずれにせよ cabal upload だと hackage2 へは アップロードできない.とりあえずのところ安心だ.とは言うものの,今後 hackage2 の開発が進んで何がどうなるかわからないので,このような環境ではWebフォームからのアップロードが安全ではあると思う.

このへんプロダクト開発において組織的に Haskell 使うようになると必要になってくるんじゃないかなー?と想像して今回のネタになったわけだが,現実として Haskell でプロダクト開発しているところでは実際のところどうなのだろうか?「えぇ!そんな目的だったら実にはコレコレこういう方法のほうがいいんデスヨ!」みたいなミミヨリ話があったら是非教えて欲しい.

孤独のHaskell

孤独のHaskellに行ってきた.ので,感想とそのフォローアップになりそうなことを書く.

とてもいい会だったように思う.

遅刻して現場に付いたらRLEしてみようという例題("AABBCCC"を"A2B2C3"にする関数を書こう)をやっていて, id:khibino0 さんがおもむろに

import Data.List ( group )
import Control.Arrow

rle = concatMap (uncurry (:) . (head &&& show . length)) . group

のような解をブッパしてたりするなど.で,各自自前実装してる人たちのコードとか見ると「細かい操作でボトムアップにやりたいことを実現しようとしてるなー」と感じることが多かった.Haskellの場合(なのかは知らないが)もっと大域的な変換からトップダウンに考えていったほうがシンプルでソレっぽいコードになるのになーと.頭から文字をひとつひとつ見ていって,次の文字と同じかどうかとか,いま何文字連長しているかとか,そういったメンドクサイ感あふれるものを一生懸命やっちゃうのはよろしくない.俺は手続きを捨てるぞォーージョジョォーッ!

というわけで,このRLEをサンプルに, hoogle と hlint に身を任せて同化することで,ほとんどトップダウンの型設計・型情報だけからかなり Haskell 的な実装にもっていく作業にトライしてみよう.

まず,欲しいものは明らかに以下のような型のrleだ,

rle :: String -> String
rle = undefined

まぁ,これだけだと何もしない関数とかも満たしちゃうから途中にあるべき状況の型を挟もう(=という設計行為をしよう).たぶん,多くの人が「「文字とその連長数」のリスト」が中間の構造として欲しいと思うんじゃないだろうか?なので,この中間構造ヘの関数toIntermediate(名前がイケてないが一時的なので)と,中間構造を解にもっていくfromIntermediateが欲しいねというハナシになる,

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = undefined

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

fromIntermediateは,さらに中間構造として「文字列(=文字と連長のペアを文字列に変換したもの)のリスト」が欲しいだろう,

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = cat . rl2strs

rl2strs :: [(Char, Int)] -> [String] -- この文字列は文字と連長のペアを文字列に変換したもの
rl2strs = undefined

cat :: [String] -> String -- 全部結合できればいい
cat = undefined

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

さて,ここまで分解して来て cat を見たらコイツは「文字列を結合する」というもう十分小さくてシンプル(と,我々には思える)処理になっている.つまり,「ライブラリのどっかにもうこれをやってくれるヤツがいるんじゃないの?」という気分になる.で, hoogle の出番だ.こいつは型で検索できる(=設計から実装を検索できる)ので,検索窓に"[String] -> String"入れて検索するといくつか出てくるが,それっぽいものを探すと上から何番目かに concat ってやつが出てきている.型は"a -> [a]"となっているが,そもそもStringが[Char]なので型は合っている.リンクを飛んでこの関数の説明を見ると"Concatenate a list of lists."と書いてあるのでビンゴだ.つまり cat は concat でいい.

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . rl2strs

rl2strs :: [(Char, Int)] -> [String]
rl2strs = undefined

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

次に rl2strs はリストの各要素をそれぞれ変換しているので,各要素に何かをやる foreach と,その何かである rl2str にさらに分解しよう.

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . rl2strs

rl2strs :: [(Char, Int)] -> [String]
rl2strs = foreach rl2str

foreach :: (a -> b) -> [a] -> [b]
foreach = undefined

rl2str :: (Char, Int) -> String
rl2str = undefined

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

同様に foreach の型について hoogle で検索すると,コイツは実には map だとわかる.

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str = undefined

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

rl2str も十分シンプルだが,コイツは hoogle してもソレっぽいものが無いので,これはこの問題特有の事情を多分に含んだ処理であり,自分で実装を与えてあげる必要があるということがわかる.まぁ,このくらいはやろう,単純だし.

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n -- n を文字列にして文字 c を先頭に付ければいいよねと

toIntermediate :: String -> [(Char, Int)]
toIntermediate = undefined

fromIntermediate 側については大体できた,次に toIntermediate 側について考えよう.やはり中間構造として「「連続した同じ文字による文字列」のリスト」が欲しいかなぁと思える.

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = toPairs . toRLs

toRLs :: String -> [String]
toRLs = undefined

toPairs :: [String] -> [(Char, Int)]
toPairs = undefined

toRLs の型で hoogle したい,が,その前に,この処理の本質を考える.すると,これって別に文字列じゃなくてもいいよね?という話になる.つまり「「連続した同じ文字による文字列」のリスト」にする処理じゃなくて「「連続した同じ何かによる何かの列」のリスト」にできる処理があれば「何か」が「文字」のときにも適用できるよねということだ. Haskell の関数は可能な限り(そのほうが便利だから)汎用的な型を与えているので "[a] -> a" で検索したほうが一層ヨサゲなものがヒットしそうだ.すると, group というピッタリなものが見付かる.コイツの検索結果の型は "Eq a => [a] -> a"だ,つまり「同じ何かによる」ということが判別できるという条件が,Eqの型クラス制約として表れている.ここまで付けて hoogle 検索するべきだったね(=設計段階で考えが及ばなかった)と反省することになる.ちなみに group は Prelude に入っておらず, Data.List モジュールの関数なので,コイツを import してやる必要があるな…と,これもドキュメントや検索結果からわかる.

import Data.List ( group )

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = toPairs . group

toPairs :: [String] -> [(Char, Int)]
toPairs = undefined

toPairs も「連続した同じ文字による文字列」を「「その文字」と「長さ」の組」にする関数をリストの各要素に適用すればよさそうだ,リストの各要素に適用する関数はもう検索した. map だったね.

import Data.List ( group )

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = map toPair . group

toPair :: String -> (Char, Int)
toPair = undefined

では,この toPair も分解してみよう.「「その文字」と「長さ」の組」を作るわけだが,「連続した同じ文字による文字列」なので「その文字」は文字列の中のどこを取っても同じなハズだ.「長さ」は単に文字列の長さがわかればいい.

import Data.List ( group )

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = map toPair . group

toPair :: String -> (Char, Int)
toPair str = (anyElem str, len str)

anyElem :: String -> Char
anyElem = undefined -- 文字列の中のどれを取ってもいいハズ

len :: String -> Int
len = undefined

anyElem もまた文字列に限らずリスト型のどこの要素を取ってもかまわないハズなので,"[a] -> a"で hoogle 検索するといくつか候補が出る. headlast だ.どっちでもいいが,リストは先頭取るほうがラクなので head を使っておこう.

import Data.List ( group )

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = map toPair . group

toPair :: String -> (Char, Int)
toPair str = (head str, len str)

len :: String -> Int
len = undefined

len もまた文字列に限らずリスト型の長さがわかればいいハズなので,"[a] -> Int"で hoogle 検索するとこれはもう length 確定.

import Data.List ( group )

rle :: String -> String
rle = fromIntermediate . toIntermediate

fromIntermediate :: [(Char, Int)] -> String
fromIntermediate = concat . map rl2str

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toIntermediate :: String -> [(Char, Int)]
toIntermediate = map toPair . group

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

お,undefinedが無くなった.from/toIntermediate もそろそろいなくなってもらって.

import Data.List ( group )

rle :: String -> String
rle = concat . map rl2str . map toPair . group

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

では,これでghci上で実行してみよう.

$ ghci RLE.hs
GHCi, version 7.4.2: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main             ( RLE.hs, interpreted )
Ok, modules loaded: Main.
*Main> rle "AABBCCC"
"A2B2C3"
*Main> rle "AABBBCCCC"
"A2B3C4"
*Main> rle "AABBBCCCCC
"A2B3C5"
*Main> :q

うん,OKぽい.さらに Haskell っぽいコードになるために仕上げに hlint を使っておこう.

$ hlint RLE.hs
RLE.hs:4:7: Error: Use concatMap
Found:
  concat . map rl2str . map toPair . group
Why not:
  concatMap rl2str . map toPair . group

RLE.hs:4:16: Warning: Use map once
Found:
  map rl2str . map toPair . group
Why not:
  map (rl2str . toPair) . group

2 suggestions

指摘が2件あった.どっちでもいいけど,まず2個目の指摘事項から修正しよう(ちょい恣意的)."map f . map g" を "map (f . g)" にしろと言われている.自然言語で言えば「「各要素にgを適用」してから,「各要素にfを適用」する」のを「各要素に「gを適用してからfを適用」する」に変換しろと言われている.要は「ループ2回書いてるのを1回で両方やるようにしたほうがいい」と言われている.

import Data.List ( group )

rle :: String -> String
rle = concat . map (rl2str . toPair) . group

rl2str :: (Char, Int) -> String
rl2str (c,n) = c:show n

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

では,改めて hlint をもういっかい.

$ hlint RLE.hs
RLE.hs:4:7: Error: Use concatMap
Found:
  concat . map (rl2str . toPair) . group
Why not:
  concatMap (rl2str . toPair) . group

1 suggestion

さっき残した 1件目の指摘だが, concatMap とやらを使えと言われている.さてこれは何かという話なのでまた hoogle だ.今度は型ではなく関数名で検索しよう. すると concatMap は名前の通り map してから concat するような処理を一度にやる関数のようだ.へぇ,と思いながら修正修正…

import Data.List ( group )

rle :: String -> String
rle = concatMap (rl2str . toPair) . group

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

で,トドメの hlint と.

$ hlint RLE.hs
No suggestions

指摘が無くなった.まぁ,ここまででももう十分ではある.だけど,まだまだ簡単にできる変換いってみよう.ヤリコンデルー.今 (rl2str . toPair) と関数合成されている.こいつを次のようにひとつの関数rle'にしてみよう.

import Data.List ( group )

rle :: String -> String
rle = concatMap rle' . group
    where
      rle' :: String -> String
      rle' = rl2str . toPair

rl2str :: (Char, Int) -> String
rl2str (c,n) = c : show n

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

ポイントフリーになっているrle'に引数を与えてみよう.

import Data.List ( group )

rle :: String -> String
rle = concatMap rle' . group
    where
      rle' :: String -> String
      rle' str = rl2str (toPair str)

rl2str :: (Char, Int) -> String
rl2str (c,n) = c:show n

toPair :: String -> (Char, Int)
toPair str = (head str, length str)

toPair を定義通りに展開できる状態になっているのでそのまま展開.

import Data.List ( group )

rle :: String -> String
rle = concatMap rle' . group
    where
      rle' :: String -> String
      rle' str = rl2str (head str, length str)

rl2str :: (Char, Int) -> String
rl2str (c,n) = c:show n

今度は rl2str が定義通りに展開できる状態になっているのでそのまま展開.

import Data.List ( group )

rle :: String -> String
rle = concatMap rle' . group
    where
      rle' :: String -> String
      rle' str = head str : show (length str)

rle'をラムダ式にしてー

import Data.List ( group )

rle :: String -> String
rle = concatMap rle' . group
    where
      rle' :: String -> String
      rle' = \str -> head str : show (length str)

その場に展開…と.

import Data.List ( group )

rle :: String -> String
rle = concatMap (\str -> head str : show (length str)) . group

よし,念のためもう一度ghciで動作をみておこう.

$ ghci RLE.hs
GHCi, version 7.4.2: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main             ( RLE.hs, interpreted )
Ok, modules loaded: Main.
*Main> rle "AAAABCCC"
"A4B1C3"
*Main> :q

hlintも.

$ hlint RLE.hs
No suggestions

問題無さそう.というわけで,最終的なコードとしては,以下のようになったわけだ.

import Data.List ( group )

rle :: String -> String
rle = concatMap (\str -> head str : show (length str)) . group

この記事最上部の日比野さんの解と比べても(&&&)を使っていないだけで殆んど同じとこまで来ているハズだ.大体このくらいの例題だったら hoogle と hlint にイロイロなアレコレを任せてしまって,自分は欲しい関数の型を考えて決めていくだけでなんとなくイイカンジに仕上がりそうかなという雰囲気を感じてもらえるだろうか.

といっても,当然だが,毎度毎度こういう設計・実装の仕方をしているわけでは無い.よく使うPreludeやその近辺の関数は普通覚えてしまっているので今回hoogleしたようなmapやconcatのような関数をhoogleすることは無い,が,これらの関数を使える機会は小さなところからボトムアップに手続き的に思考していこうとするとあんまり出てきてくれなかったりする.それよりも,hoogle(や,あるいは hayoo)等で出てきそうかなと思えるまで問題を型で分割していくように考えたほうがシアワセになり易い.と思う.

sigma.jsでhackageのパッケージ依存関係を視覚化する

sigma.jsを使ってみたかったので,hackageの(baseを除外して)パッケージ依存関係を視覚化してみた


頂点(=パッケージ)数が多くてForceAtlas2による頂点配置がある程度安定するまでが長いしFPSも低い.でもまぁじっと待ってればそれなりになる.最初は各パッケージ各バージョン毎に頂点としてたけれど,それだともうGEXFグラフデータが80MB越えてきたりしてダウンロードから既にイッパイイッパイだし,結構な確率で表示処理中にブラウザが落ちてたので諦めた.参照数多いパッケージの頂点を大きくしたり色付けたりしたいところではある

ソースはここ.00-index.tar.gzをひろってきて,各パッケージの.cabalから依存関係解析したグラフをGEXFに変換してsigma.jsに食わせるだけ.そんなイロイロやるコトあったわけじゃないけどhakyllも試してみてる.

Boost.Preprocessorを使う

ghcはC preprocessorを使える.となると,より一層強力なマクロを求めてBoost.Preprocessorを使ってみたくなるかもしれない.いや,やっぱそんなことないかもしれない.

ともかくサンプルコード

{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -F -pgmF ./cpp.sh -I/usr/include #-}
import Text.Printf

#include <boost/preprocessor.hpp>
#define FOLDL(op, e, ls) BOOST_PP_LIST_FOLD_LEFT(op, e, ls)
#define ADD(d, x, a) BOOST_PP_ADD(a, x)

main :: IO ()
main = do
  printf "line: %d\n" (__LINE__ :: Int)
  print FOLDL(ADD, 0, (1, (2, (3, BOOST_PP_LIST_NIL))))

でコンパイルには次のスクリプトcpp.shが必要

#!/bin/sh
/usr/bin/cpp $1 -o $3

どうも単にpreprocessorを通すだけのオプション設定(-XCPP分のみでcpp.sh無しとか)だと,プリプロセッサマクロの深い展開ができないみたい?なので,今回はcpp.shを用意して,明示的に素のpreprocessorを通すようにしている.

これで,めでたくbuildがboostに依存する謎のHaskellコードが作成できた.一方ロシアはTemplateHaskellを(ry

第4回 スタートHaskell2

第4回 スタートHaskell2でLightning Talkしてきた.LTとはいえ自分しか枠使う人いなかったので,ゆったり時間使わせてもらえた.主催陣に感謝.

ちょうどIOの回だったので(というかここまで来るのを待ってLT枠もらったので),ごるふ場への勧誘という意図でふいんき伝える程度の軽い内容をば.

新規勢が増えるよ,やったね以下略.