どれほどのHackageにChangelogがないのか調べてみたHaskellで
Haskellを勉強し始めて驚いたのが、あまりにも多くのHackage(Haskellにおけるライブラリパッケージ)にChangelogが存在しないことだ。
当たり前だが、Changelogがなければモジュールのバージョン間の変化を俯瞰することができない。Haskellの文化では互換性を破壊するモジュールの変更がある程度許容されるそうなので、このことは著しい不便を生むことがある。(ただし、Cabalパッケージ間の変化を調査するprecisというプログラムもあるようだ)
しかし、「ChangelogのないHackageが多い」というのはあくまで主観的な印象であり、そのようなHackageが正確にいくつあるのかは分からなかった。そこで、Haskellの練習がてらこれを調べてみた。
調査方法
Hackageの公式レポジトリはhackage.haskell.orgであり、現在公開されているHackageの全リストは http://hackage.haskell.org/packages/ から取得できる。このエンドポイントはリクエストヘッダに"Accept: application/json"をつけるとJSON形式でリストを返すので、今回はこれを利用した。
hackage.haskell.orgにてインデクスされるChangelogを取得するには、 http://hackage.haskell.org/package/パッケージ名/changelog にアクセスすればよい。changelogが存在すればその内容が、なければ404 Not Foundが返される。今回はChangelogの有無を確認すればよいだけなので、GETリクエストではなくHEADリクエストを使った。
なお、hackage.haskell.orgのAPIは http://hackage.haskell.org/api で公開されている。
もちろん、この調査方法は完璧ではない。パッケージ内に(haddockなどで)変更履歴がきちんと記載されているのにChangelogファイルがない場合もあるだろうし、逆にChangelogファイルはあるが中身が空の場合もあるだろう。細かな数字はあくまで参考程度にすべきである。
結果
2015/02/16現在の調査結果が以下の通り。この時点での全Hackage数は7670個だった。
比較
参考までに、Perlのモジュールパッケージ(「ディストリビューション」と呼ばれる)でもChangelogの有無を調べてみた。これはわざわざプログラムを書くまでもなく、以下のサイトで統計データを閲覧できる。
- CPANTS - Kwalitee - has_changelog http://cpants.cpanauthors.org/kwalitee/has_changelog
ただし、集計方法は上記のHackageに対する方法と異なる。このサービスでは、年ごとにアップロードされたディストリビューションについてChangelogの有無を集計している。ここで、アップロードされたディストリビューションとは、全くの新規ディストリビューションの場合もあるし、既存のもののアップデート版の場合もある。
また、"Fail"の項目が3種類ほどあるが、"Total Uploads = Pass + Fail (BackPAN)"という関係になっているようだ。
つまり年ごとにアップロードされたディストリビューションについてChangelogがないものの比率は"Fail (BackPAN)"のカラムを見ればよい。すると、だいたい5.0% 〜 7.3%程度で推移しているようだ。
上述のように、集計方法が異なるため現在累積されているディストリビューション全体についてのデータはないが、ほぼ同じ程度の値であると推測される。
考察
「半々くらいならしょうがないかなあ」と考えていたが、想像以上にChangelogのないHackageが多い結果となった。Perlモジュールと比べると実に10倍以上である。
なぜこれほどChangelogがないのだろうか?思うに、cabal initコマンドがChangelogのテンプレを生成しないのが良くないのではないだろうか。cabal-installの最新バージョン(1.22.0.0)で試してみたが、Changelogは生成されなかった。
後でcabal-installのソースコードを見て、この点を修正できないか調べてみようと思う。
調査プログラム
以下、今回調査に使ったプログラムを示す。
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} import Prelude () import BasicPrelude import qualified Data.Aeson as Aeson import qualified Network.HTTP.Conduit as HTTP import qualified Data.Map as M import qualified Formatting as F data PackageInfo = PackageInfo { piName :: Text, piChangelogExist :: Bool } deriving (Eq,Ord) showPInfo :: PackageInfo -> Text showPInfo p = piName p <> ": " <> show (piChangelogExist p) reqBase :: ByteString -> Text -> HTTP.Request reqBase method api_path = let url = "http://hackage.haskell.org" <> api_path req = either (error . textToString . show) id $ HTTP.parseUrl (textToString url) in req { HTTP.method = method } reqJson :: HTTP.Request -> HTTP.Request reqJson req = req { HTTP.requestHeaders = (("Accept", "application/json") : HTTP.requestHeaders req) } reqGetJson :: Text -> HTTP.Request reqGetJson = reqJson . reqBase "GET" reqHead :: Text -> HTTP.Request reqHead = reqBase "HEAD" getPackageNames :: (Functor m, MonadIO m) => HTTP.Manager -> m [Text] getPackageNames httpman = do res_body <- HTTP.responseBody <$> HTTP.httpLbs (reqGetJson "/packages/") httpman let key :: Text key = "packageName" e_names = (maybe (Left $ "missing packageName") Right . mapM (M.lookup key)) =<< Aeson.eitherDecode res_body case e_names of Right names -> return names Left err -> fail err checkChangelog :: MonadIO m => HTTP.Manager -> Text -> m Bool checkChangelog httpman package = liftIO $ do HTTP.httpLbs (reqHead ("/package/" <> package <> "/changelog")) httpman return True `catch` (\e -> case e of HTTP.StatusCodeException _ _ _ -> return False) getNameFilter :: [Text] -> [Text] -> [Text] getNameFilter [] = id getNameFilter (num :_) = take $ read num main = do name_filter <- getNameFilter <$> getArgs HTTP.withManager $ \httpman -> do names <- name_filter <$> getPackageNames httpman pinfos <- forM names $ \name -> do pinfo <- PackageInfo name <$> checkChangelog httpman name putStrLn $ showPInfo pinfo return pinfo let total = length pinfos (oks, ngs) = partition piChangelogExist pinfos ok = length oks ng = length ngs (ok_perc :: Double) = (fromIntegral ok) * 100 / (fromIntegral total) (ng_perc :: Double) = (fromIntegral ng) * 100 / (fromIntegral total) liftIO $ F.fprint format "OK" ok total ok_perc liftIO $ F.fprint format "NG" ng total ng_perc where format = F.stext .": ". F.int ." / ". F.int ." (". F.fixed 2 ." %)\n"
練習のため、今回は自分にとっていろいろ新しい試みをしてみた。
basic-prelude
"better Prelude"であるbasic-preludeを使ってみた。使ってみてよかったのは、
http-conduit
HTTP通信にはhttp-conduitを使った。
最初はHTTPを使ったが、標準のAPIがStringベースだったり、ちょっとイケてないところがあるように思う。http-conduitは非2XXレスポンスをデフォルトでIO例外にするという思い切った作りだが、そっちのほうが使いやすいと思う。
今度機会があればwreqも試してみたい。
formatting
"better printf"であるformattingを試してみた。
Text.Printfは引数の型や数が合わないと実行時例外を投げたり、StringベースAPIだったりいろいろキツいところのあるモジュールである。formattingは引数仕様に型チェックが効き、結果をTextで返してくれる。
それはいいんだけどフォーマット文字列の表記はお世辞にも読みやすいとは言えない。もうちょっとなんとかならんだろうか。