• 作成:

Amazon S3の問題はMultipart Uploadで解決しそう, haskellの文字列の変換をわかりやすくするstring-transformを公開しました

Amazon S3にファイルがアップロード出来ない問題はそもそもPutObjectが大容量のファイル向けじゃないからのようでした

conduitのMonadResource mをIOに変換する方法がわからない - ncaqについて, @as_capablさんに助言を貰ったので, それについて調べてみました.

昔あった関数をそのままコピーすることは簡単ですが, 何か問題があって消えた関数かもしれないので, 消えた経緯を調べてみます.

http-conduit :: Stackage Serverにgithubへのリンクが貼られてなくて面倒. ソースはsnoyberg/http-client: An HTTP client engine, intended as a base layer for more user-friendly packages.にありました.

見てみるとrequestBodySourceChunked :: Source (ResourceT IO) ByteString -> RequestBodyがありました. どうもNetwork.HTTP.Client.ConduitではなくNetwork.HTTP.Client.Conduitの方にはそれがあるらしいですね.

試しにimport qualified Network.HTTP.Conduit as LowしてLow.requestBodySourceChunked $ fileSource fileInfoしてみたらあっけなく型チェックが通りました.

Network.HTTP.Client.ConduitのコメントにA new, experimental API to replace "Network.HTTP.Conduit".と書かれているので不安を感じますが, low-levelなAPIではNetwork.HTTP.Conduitを使うべきだということでしょうか. 使わないとストリームが出来ないので是非もなし.

と思って実装してみたら, S3Error {s3StatusCode = Status {statusCode = 501, statusMessage = "Not Implemented"}, s3ErrorCode = "NotImplemented", s3ErrorMessage = "A header you provided implies functionality that is not implemented"} とかいうエラーが出てきて通りませんでした. は?

1KBのファイルを投稿してみても通らないので負荷とか関係ないみたいですね.

おそらくエラーを出しているのはaristidb/aws: Amazon Web Services for Haskellなので, これを見ていきましょう. 何処を参照すればいいのかわからないのでgithubのリポジトリをエラーメッセージで検索してみます.

検索してみたらコードは引っかかりませんでしたがissueが引っかかりました. PutObject Error · Issue #85 · aristidb/aws RequestBodySourceChunkedはAWSの制限で使えないらしいですね.

じゃあ私にどうしろというのだ… メモリ増設するしか無いと?

どうしよう… と思ってググってみたら, Go言語のaws-sdk-goのissueでmultipart uploadを使うべきという助言がありました. s3.PutObject: A header you provided implies functionality that is not implemented · Issue #122 · aws/aws-sdk-go しかし, このissueによると, multipart uploadは5MBが下限だそうです.

awsの公式サイトにもmultipart uploadを使えと書いてありますね. 1GBクラスのファイルをPutObjectでアップロードしようとしていた, 私のawsのAPIの使い方がそもそもおかしかったようです.

通常、オブジェクトサイズが 100 MB 以上の場合は、単一のオペレーションでオブジェクトをアップロードする代わりに、マルチパートアップロードを使用することを考慮してください。

Multipart Upload API を使用したオブジェクトのアップロード - Amazon Simple Storage Service

しかし, ストリーミングデータを全て読み込まずにストリームしてアップロードしたいのに, どうやってサイズを知れば良いんだ… と思いましたが, HTTPの要求ヘッダにContent-Lengthがありました.

余談ですが, ちょっと調べてみたら

ってか、GB単位を http で upload とかバカじゃねーの!

大規模ファイルアップロード時のブラウザ対応状況私的まとめ - いろきゅう.jp ~Programmable maiden~ Tech side

という言及がありました. そうなのかもしれない… しかし, これは2012年の話なので, 今のwebなら問題ない気もします.

logDebugがパースエラーになると思ったらインデントを間違えていました

デバッグ用に$logDebugを設置しているのですが, parse error on input ‘$logDebug’というものが出まくっています. なぜ?

{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE TemplateHaskell       #-}

というプラグマは忘れずに配置しているのですが… Control.Monad.Loggerの公式ドキュメントを読んでみたら$(logDebug)としているので変えてみたらparse error on input ‘$(’が出てきます. do記法と組み合わせると食い合わせが悪い(?)のかなあ… Foundation.hsでは問題なく$logDebug $ "Copy/ Paste this URL in your browser: " <> verurlで動いているのですが. Template Haskellを使うとエラー表示が過剰に出てくるか, parse errorの一言だけを出すか, 両極端すぎると思います.

よく見てみたらdo記法でスペースが1つ足りなくてインデントが揃っていなかった. うおおおお. 普段はEmacsのhaskell-modeがオートインデントしてくれるのでこんな自体にはならないのですが, Template Haskellが絡むと解析しきれないのか, オートインデントが働かなくなります. そしてこういうことになったりします.

haskellの文字列の変換をわかりやすくするstring-transformパッケージを作って公開しました

classy-prelude環境でByteStringStringに変換するにはどうすれば良いんだろう? もちろんutf8-stringを使えば返還できますが, classy-preludeは大抵の関数を用意していてdecodeUtf8 :: ByteString -> Textなどはあるので, 出来ればその範囲内に収まりたいという気持ちがあります. haskellの文字型変換は極めて複雑になっていて, 全く覚えられません. いっそのこと自分で変換ラッパーパッケージを書いてしまおうと思いました.

事前に調べたところ, String, ByteString, Textを一括のclassdataで上手く扱おうというライブラリはたくさんありましたし, classy-preludeの様に抽象的に変換を扱おうというライブラリはたくさんありました. しかし, そういうのって抽象度が高くて応用性はあるのですが, 名前は抽象的になってわかりにくいんですよね. 同一の関数上で型クラスを使って多様な変換を実現しているのですが, それにより私は結局使うべき関数名がわからなくなってしまいます.

具体的には, decodeUtf8encodeUtf8のどちらを使えば良いのか覚えられなくていつも混乱しています. 私は物覚えがそんなに良くないのです. decodeとencodeがそれぞれのデータ型に結びつきません. いつも大混乱です.

なので, 私はあえて単純な関数名を使ったライブラリを構築してみたいと思います. ライブラリというより, ラッパー程度ですが.

ただのラッパーなので, 無くてもそれぞれのモジュールをロードしてpackなどを使えば実現できるものですが, 単に変換したいだけなのに型に一致したモジュールを読み込んで, 関数を選ぶのはとても面倒くさいものです. やろうとすれば出来ますが, とにかく面倒くさいし, プロジェクトによってモジュールプレフィクスが違ったりしてきます. 変換関数を使っているところのコードを見ても, importを見ないと何をやってるかわからないのは可読性が悪いです.

アホにもわかるライブラリを構築したい.

しかし, 機械でも生成できそうなただのラッパーでも, データ構造が大量にあるので, 組み合わせが多量になってしまい書くのが面倒でした. 面倒さを回避するための面倒なので良いのですが…

toByteStringといった名前を使おうかと思いましたが, これらは既にライブラリが占拠していたのでtoByteStringStrictという名前にしました. こちらのほうがデータ型が関数名からわかりやすいですし. ハンガリアン記法(間違った使い方, システムハンガリアン記法の方)という悪夢を思い出しましたが, これは区別を容易にする必要があるので仕方がないでしょう.

toStringもたくさんありますが, これは他に名前が無いので仕方がない.

パッケージ名をstring-convertにするかstring-transformにするかで悩みました. convertの方が単純な変換を意味しているそうなのでこちらを選びました.

ひとまず一定の関数を満たしてgithubにアップロードしました.

一応safe haskellにしました.

hackageにアップロードする前に一応テストを書いておきましょう.

stack templatesにtasty-travisがあるように, 今はhspecよりもtestyの方が人気なのかな? hspecを使い続けようかと思ってましたが, 1回小さなプロジェクトでtastyを使ってみますか, hspecに不満が無いわけではないですし.

そんなことを言ったらtastyで結局hspecを内部で使ってるという話があり, 別にtasty使う意味無いのかなあと思いつつも使ってみないとわからないので使ってみます.

使ってみると, SmallCheckはいいけど, HUnitはかなりわかりにくいのではという感想を抱きました.

tasty-discoverというのがあるらしいですが, あいや☆ぱぶりっしゅぶろぐ! - tasty-discoverを使ってみた hspecにも同等のものがあって今使ってるんですよね… それに比べてtastyのいいところは, テスト結果が読みやすいところでしょうか.

stack upload .してみたら, string-convert: Universal string conversionsが既に存在しているということが判明しました. stackageの方で検索して存在しなかったから油断していました…

仕方がないので名前を変えることにしました. 何がいいだろう… text-convertは存在しないけど, String, ByteString, Textを取り扱うので嫌だ… 没になったstring-transformにしておきますか… 殆ど同じ意味なので, 別にいいや, 悔しくなんかない.

というわけで, ncaq/string-transform: simple and easy haskell string transformを公開しました. hackageの方はstring-transform: simple and easy haskell string transformです. しかし, 実装するのは非常に簡単なのに, 今まで誰もこれを書いてこなかったのは不思議ですね.

OverloadedRecordFieldsは既に実用レベルにあるのかもしれません

haskell 2010は重複したフィールドラベルを定義できません.

なので

data Point2 = Point2 { x :: Double, y :: Double }

と定義したくても, プレフィクスを付けて

data Point2 = Point2 { point2X :: Double, point2Y :: Double }

と定義したりする必要がありました. 泣けますね.

awsとかは, GetObjectコンストラクタの場合はgoと, コンストラクタの略文字をラベル名に付けていました.

そのへんの話は以下に詳しいです.

それで, 一応使えるけどIsLabelの導出を手で書かないといけないの滅茶苦茶面倒だな… 自動導出が入らないとやってられないな… と思いながら以下のようなサンプルを書いていて気が付きました.

パターンマッチでの値の取り出しならIsLabelの定義を書かなくてもフィールドを取り出せます. 基本的に私はパターンマッチでフィールドを取り出すので, IsLabelが無くても十分使える気がしてきました. NamedFieldPunsがあればレコードのパターンマッチも簡潔に書けます.

次にデータ型の定義をするときにはDuplicateRecordFieldsOverloadedLabelsを付けてプレフィクス無しでやってみようと思います.

ただ, まだPureScriptみたいに特定のラベルを持つデータ型をジェネリックに指定できないのは残念ですね.

MagicClassesが入れば出来るようになるらしいですが, PureScriptに比べて型の見栄えが悪いように見えるのは気のせいでしょうか.