• 作成:

コマンドラインツールの例外実装, string-transformとOverloadedStringsの相性が悪い, Multipart Upload

コマンドラインツールの例外実装

テストできるようにモジュールをlibraryとexecutablesに分けました.

baseのexceptionを使うよりsafe-exceptionsを使うほうが良いそうなのでimport先だけ変えておきます. 現在非同期例外を使うことはないのであまり恩恵は無いかなあ… と思いましたが, tryにたくさん型注釈をつけていたところをtryAnyにすることが出来ておおこれは便利.

昨日書いたstring-transform: simple and easy haskell string transformをこのプログラムにも使うことにしました. 単純なモジュールですが, もっと広まって世の中のhaskellコードの可読性が上がって欲しい.

プログラムをDuplicateRecordFieldsOverloadedLabelsを前提に書き換えています. しかし, パターンマッチで取り出すことを前提にやっていると, 同じスコープで重複したラベルを扱う時, 結局はプレフィクスを付けてパターンマッチさせて重複させないということが必要なことがわかってきました.

コマンドラインオプションを取り扱う時Maybeが出まくるのでのエラー処理をしてMaybeを取り外す時に意味は同じなんだから同じ変数名を付けたくなります. しかしhaskellでは変数名をシャドウするのは良くない習慣として取り扱われているのでダメです. Maybe付きのラベルにmプレフィクスを付けてしまうのが常道なんでしょうかね… 逆にMaybeを取り外したものにjustプレフィクスを取り付けてしまった. ローカル変数名なんて別にどうでもいいんですよ.

Prelude.read: no parseを例外を引いて, tryAnyで囲ってるのに何故例外が出てくる? と思ったらやっぱり遅延評価が原因みたいですね. haskell - Can't catch "Prelude.read: no parse" exception with Control.Exception.try - Stack Overflow はじめて遅延評価由来のバグを引いた気がします. そんなことはないか? ここに「tryAnyDeepを使えば良いですよ」と書き込んだら-1された… 悲しい…

deepseqのforceを使って評価しようかと思いましたが, safe-exceptionsに tryAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => m a -> m (Either SomeException a) というまさに求めている関数がありました. 結局NFDataのインスタンスを要求するので, deepseqに依存する必要があるんですけどね. evaluateの場合IOになってしまって面倒な代わりにbaseで依存が済むようですね…

ラベルの重複は問題なくなったけど, 型コンストラクタの重複は問題になるのでやはりプレフィクスを付けざるを得ない.

ついに(やっと?)haskellでerrorを使っていた所を独自の例外に書き換えようとしていますが, エラー表示に悩んでいます.

errorをそのまま使っていれば, 例外を最終的に処理するときに渡した文字列を表示してくれるのですが, Exceptionthrowすると, Exceptionの中にStringを入れてようが無視して表示されてしまいます.

ちゃんとエラーをユーザに表示するには以下の2つの方法があるでしょう.

  • 上位層で例外をキャッチして例外に対応した文字列を表示する
  • エラー表示をしてから例外を出力する

今回は後者を選ぶことにしました.

httpエラーの時にはレスポンスを表示するようにしているのですが, これがフィールドが結構多いのでshowそのままだと大変見にくい. 仕様には見やすくする必要があるとか入ってないのでやる必要は無いのですが, 流石にこれはどうかと思ったので何かpretty printをしておく必要がありそうですね. cdepillabout/pretty-simple: pretty-printer for Haskell data types that have a Show instanceがちょうど良さそうですね. pPrintstderrに出力出来ませんが, まあpShowすれば良いだけですね. pShowだとカラーにならないかもと思いましたが, なりました.

string-transformとOverloadedStringsの相性が悪いがどうにもならなかった

string-transformを実際に使っていったところ, 型推論が決定せずにAmbiguous type variableになるケースがあることがわかってきました. 渡す関数がStringTextかどっちを返すかわからないというケースは多分仕方がないです. しかし, OverloadedStringsを有効にしている時に文字列リテラルを渡すとString, ByteString, Textが候補に上がって決定できないというのは厳しすぎます. 本来OverloadedStringsを有効にしていたら文字列リテラルから文字列を変換する必要は無いはずなのですが, 高度に型クラスを使用していると必要になる時があります. もちろん, その時は本来文字列リテラルに型注釈を付けるのが正解なのですが, 変換を指定された時に動くようにしておきたいという欲求もあります. 解決策を考えます.

ぱっと思いつく解決策はIsString a向けのインスタンスを追加してみることです. いや, ダメですね, IsStringはあくまでfromString :: String -> aStringから何かを生成するためのクラスなので, これでインスタンスを構成することは出来ません.

数値型なんかは複数選択肢があってもIntに定まったりするので, 何かデフォルトを指定するプラグマがあるんでしょうか. 参考にしようと思って見てみましたが, GHC.NumGHC.FloatGHC.Realも実装のソースコードがstackageから見れない… ExtendedDefaultRulesとかいうのがあるらしいのでこれかなあと思って見てみましたが, defaultの指定方法がわからない.

githubからghcのフルソースコードを持ってきました. これを読む. しかし, defaultが設定されているようには見えません…

あれ, もしかしてghciだとdefaultが定まるだけで, 本物のソースコードでdefaultを設定する方法は存在しない…?

Defaulting – Haskell Primeにhaskell 98の頃の提案が書かれていました.

同じようなことをやっている人が居ました. haskell - Why am I forced to specify a type here? - Stack Overflow

ExtendedDefaultRulesはghciだとデフォルト指定なんですね. 結局これを指定してお警告は消えないので, ダメそう.

結局実害は殆どないようなので放置することにしました. 一部のところで関数に型注釈しなければいけないのはfromを省略してtoだけ指定するようにしている都合上仕方がない.

文字列リテラルから変換できないのは文字列リテラルに型注釈を付けるべきということですね.

Multipart Uploadでのアップロードを実装しました

補助ライブラリの開発に夢中になって, 本題を忘れかけてしまっていましたね. Multipart Uploadの実装を進めていきましょう.

とりあえず小さいサイズのファイルをこれまでのようにPutObjectするためにContent-Sizeを取得して比較するなんてことをやっているのですが, これバイトサイズなので気をつけないと32bitを超過してしまいますね. HaskellならIntになるのを警告してくれますし, Int64Integerもあるから大丈夫ですが. オーバーフローしてしまうブラウザの実装がたくさんあるのも納得です. 32bit環境で動くブラウザで大規模数扱うの, gmpがないとつらそう.

サンプルコードを見ようと思ってaws/MultipartUpload.hs at master · aristidb/awsを見てみましたが, チャンクサイズとかの調整が知りたかったのに全部コマンドライン引数に任せているのでほとんど参考にならない… 思わずamazonkaに変えようかと思って見てみましたがこちらはLensしている上サンプルコードが全く無かったのでやめました.

awsの中にもたくさん関数があって何を使えばいいのかさっぱりわからない, とりあえずサンプルで使われていたmultipartUploadSinkを使ってみます.

multipart uploadというぐらいだから数回接続をするんだろうなあと思ってsimpleAwsではなくpureAwsを使おうと思いましたが, multipartUploadSinkは他のawsパッケージの関数とは違って自前で実行まで行うようですね.

適当にS3.multipartUploadSinkで書いていたら型チェックが通りました.

ソースを一部公開すると以下のような感じです.

    withManager $ do
        mgr <- ask
        fileSource fileInfo $$
            S3.multipartUploadSink appAwsConfiguration Aws.defServiceConfig mgr appS3Bucket
            (fileNameOfKey key) (10 * 1000 * 1000) -- 10MB

もしかして動くんじゃないか…? と期待しながら動かしてみます.

動かしてみたら100MBのファイルは普通に分割アップロードできました.

しかし, 1GBのファイルをアップロードしてみると, ConnectionTimeoutが発生してしまいました.

もう一度やってみたら今度は動いているようです, 前回はネットワークインスペクタを開いてたから大量のリクエストボディを見てしまって破綻してしまったのでしょうか.

しかし, 1GBのファイルアップロードにかなり時間がかかりますね… 回線が悪いのかな? 並列実行しないとパフォーマンスが出ないのでしょうか…? それともメモリが足りないからswapにアクセスしてしまっているのでしょうか.

やっぱりユーザのブラウザから直接アップロード出来るようにした方が良さそうですね.

こっちの方が進行状況をブラウザで表示するのも容易ですし, こちらにしてしまいましょうか. 同期問題もcompleteして初めてデータベースに登録すればまあ現実問題にならなさそう. 問題はcompleteしたことをサーバに伝えずに延々ファイルだけ送信してくるクラッカーが居た場合ですが, 対策は

  • ジョブを動かしてデータベースに登録されてないS3ファイルを定期的に消す
  • forkHandlerで待ちスレッドを作り, 一定期間待ち, completeが来なければ削除してしまう
  • そもそもcomplete通知をブラウザに任せずにS3のイベント通知を利用する

などの対策が考えられますね. しかし, データベースへの登録はファイルが送信し終わってからでないと不都合が生じるんですよね. それでいてファイル名はデータベースのIDに依存するのでcommitは開始しないといけない. completeフラグを生やしてcompleteしていないレコードは無いとして扱うとかで済ましましょうか? うーんやはり難しい.

まあ, それは後で良いでしょう. まずはダウンロードだけでもwebサーバを介さない方法に切り替えたい. 今はアップロードができる, それだけで十分でしょう.

チャンクを100MBに増やしてみましたが別に高速にはなりませんね…