haskell-jp / questions #26

なるほど。その logLevel 定数のありかが気になってました。
一時ディレクトリーにログを書き込むってことか。
Kan extension は何か具体的な用途や例などあるの?
最近 Kan extensionを勉強して、なんとか理解できたけど、実際の用途は全然思いつかなくて…
ありがとうございます。
以下のコードをビルドすると Orphan instance: instance From Word8 HogeSerializeFormat という警告が出てしまいます。
https://repl.it/repls/SilverIntentEmulator
{-# OPTIONS_GHC -fno-warn-orphans #-} をつければ警告を消せるようなのですが、そもそもこの警告は何が問題なのでしょうか?(実行時にエラーになる場合があるなど)
ネットで調べてみると instance From Word8 HogeSerializeFormat の部分のファイルを分ければよいということが書いてあったのですが、分けようとした(module Data.Word内に書いてみた)際に循環importになってしまいビルドができなくなってしまいました。
hs-bootというものを使えば解決できると書いてあったのですが、僕の理解が間違っているのかhs-boot内でお互いをimportしてしまい、結局循環import問題が発生してしまいました。
長くなりましたが、この問題はどう解決するのがベストなのでしょうか? {-# OPTIONS_GHC -fno-warn-orphans #-} をつけて警告を消してしまってよいのか、それとも別の方法がよいのか。
よろしくお願い致します。
{-# OPTIONS_GHC -fno-warn-orphans #-} をつければ警告を消せるようなのですが、そもそもこの警告は何が問題なのでしょうか?

実行時エラーが起こると言うことはありません。
この警告は、
- 「ある型クラスの定義」と「ある型に対するその型クラスのインスタンスの定義」
- 「ある型の定義」と「その型に対するある型クラスのインスタンスの定義」
がすべて別のモジュールに分かれてしまっている場合に起こります。
今回の場合、
instance From HogeSerializeFormat Word8

などのインスタンスの定義が問題になっているわけですが、
これは、 Word8 という型の定義と、 From という型クラスの定義、どちらのモジュールとも異なるモジュールで宣言しているから Orphan instance と言われてしまいます。
インスタンスの定義だけが型クラスの定義からも型の定義からも離れた、 "orphan" (孤児)になってしまっているから "Orphan instance" といいます。
で、何が問題なのかというと、どのモジュールを import したらいいのかわかりづらくなってしまうという問題があります。
Word8 について From のインスタンスがあることを知っていても、 Word8 が定義されたモジュール、 From が定義されたモジュール、どちらを import してもインスタンスが利用できないため、使おうとしたときに迷ってしまう恐れがあるのです。
実際のところ、orphan instanceは意図的に定義されることもしばしばあります。
そういう場合のために、警告として、無効にできるようになっているのです。
例えばいろいろな型に使える、汎用性の高い型クラスを提供するパッケージAがあるとします。
Aの作者はできるだけいろいろな型をその型クラスのインスタンスにしようとしますが、実際にはあらゆる型をサポートできるわけではありません。
Aがリリースされた後に新しい、Aが提供する型クラスにふさわしい型がリリースされるなんてことは十分あり得ますし、そもそそもあらゆる型をサポートするために、Aの依存パッケージを増やすということは、できるだけ避けるべきです。

それでもAが提供する型クラスのインスタンスを、あの型にも提供したい!という場合に、orphan instance専用のパッケージが作られることがしばしばあります。
「hogehoge-instances」なんて名前のパッケージがあったら、多くの場合そうしたorphan instance専門のパッケージです。
探してみると面白いでしょう。
http://hackage.haskell.org/packages/search?terms=instances
今回の場合は、 instance From HogeSerializeFormat Word8 などを Data.From モジュールに移動させれば、
「ある型クラスの定義」と「ある型に対するその型クラスのインスタンスの定義」
が同じモジュールに含まれるようになるため、問題ないはずです。
後で調べてわかったんですが、Coercibleはユーザーが自分でインスタンスを定義できるものではなく、GHCが自動でCoercibleかどうか判定するという非常に特殊なものなので、RustのFromとはずいぶん性格が違うようです。。。
なるほど! Data.From に移動すればよかったのですね!
移動したところ警告が出ずにビルドすることができました!
丁寧にご説明していただき、ありがとうございます!
Orphan instanceを避けるべき理由としては他に、ライブラリaとライブラリbがそれぞれ勝手にinstance宣言をして、それらが被っていると、それらを同じプロジェクトで使った時に問答無用で重複エラーになる、すなわちライブラリaとbを同時に使う事が出来なくなる、というのがありますね。なので、ライブラリでなくexeのソースでやる分には結構大丈夫なんじゃないかと思ったりします
GADTの計算するときに使えるときもあります
ライブラリでやりたいので注意してやっていこうと思います!
@sakuemon has joined the channel
lagrange.resolvent
@lagrange.resolvent has joined the channel
@yuichi has joined the channel
Conduit(cereal-conduit)について質問させてください。
cereal-conduitで定義されている conduitGet2 を真似して(ほぼコピペ)デシリアライズした値と一緒に、今何バイト目にいるのかをタプルで返すコードを書いてみたのですがコンパイルエラーになってしまいうまく行きません。
i <- IO.hTell h の部分を i <- return 123 などにするとコンパイルはできたのですが、なぜ i <- IO.hTell h だとエラーになってしまうのかがわからない状況です。
上記の質問とは少しずれてしまうのですが、自分で conduitGet3 などを定義せず既存の conduitGet2 を使いつつ今何バイト目にいるのかをデシリアライズした値と一緒に返す方法なんてあったりするのでしょうか?
ちなみにエラーメッセージはこんな感じです。よろしくお願い致します。
    • Couldn't match type ‘IO’
                     with ‘ConduitT BS.ByteString (o, Integer) m’
      Expected type: ConduitT BS.ByteString (o, Integer) m Integer
        Actual type: IO Integer
    • In a stmt of a 'do' block: i <- IO.hTell h
      In the expression:
        do i <- IO.hTell h
           yield (x, i)
           if BS.null rest then awaitNE >>= start else start rest
      In an equation for ‘result’:
          result (Done x rest)
            = do i <- IO.hTell h
                 yield (x, i)
                 if BS.null rest then awaitNE >>= start else start rest
    • Relevant bindings include
        x :: o (bound at app/Main.hs:73:18)
        result :: Result o -> ConduitT BS.ByteString (o, Integer) m ()
          (bound at app/Main.hs:66:5)
        awaitNE :: forall o. ConduitT BS.ByteString o m BS.ByteString
          (bound at app/Main.hs:54:5)
        g :: Get o (bound at app/Main.hs:49:15)
        conduitGet3 :: IO.Handle
                       -> Get o -> ConduitT BS.ByteString (o, Integer) m ()
          (bound at app/Main.hs:49:1)
   |
74 |         i <- IO.hTell h
   |              ^^^^^^^^^^
まず単純なエラーメッセージの解釈ですが, IO.hTell hIO Integer 型になりますが result :: Data.Serialize.Result o -> ConduitT BS.ByteString (o, Integer) m () 型になるため, do 構文の制約により ConduitT BS.ByteString (o, Integer) m モナドでなく IO モナドを使っているためエラーになります. return 123 にするとうまくいくのは, return 123 :: ConduitT BS.ByteString (o, Integer) m Integer となるためです.
なお,型がどう推論されているかは,Type holeという機能を使って result :: _ -> _ とすればエラーメッセージとして表示されるようになります.
それで修正案に入る前に,要件の確認なのですが,この関数は,
runConduit $ sourceHandle h .| conduitGet3 h .| sinkList
というような感じで sourceHandle に渡されたハンドルを conduitGet3 にも渡される前提で使用するという想定で合ってますか?
夜分遅くにありがとうございます!
はい!そんな感じです! runConduit $ CC.sourceHandle h .| conduitGet3 h (get :: Get Value.Value) .| CC.mapM_ print で画面に表示されるか試していました!
Type hole…知りませんでした…!
これは,HandleのPositionでなくてはだめでしょうか?例えば,Data.SerializeのbytesReadを使うと,
conduitGet3 :: MonadThrow m => Get o -> ConduitT BS.ByteString (o, Int) m ()
conduitGet3 g = conduitGet2 (twoOf g bytesRead)

みたいなことができます.
HandleのPositionにこだわりはありません!今何バイト目にいるのかがわかれば問題ありません!デシリアライズ対象のデータの先頭が何バイト目なのかが知りたいだけですので!
twoOf という関数をググっても出てこないのですがこれはどこで定義されている関数でしょうか?
いちよ,現状の conduitGet3 を修正する場合, http://hackage.haskell.org/package/base-4.11.1.0/docs/Control-Monad-IO-Class.html を使用して hTell h の部分を liftIO (hTell h) にすれば動くはずです.
あ,すいません,s/twoOf/getTwoOf/ です. http://hackage.haskell.org/package/cereal-0.5.7.0/docs/Data-Serialize-Get.html#v:getTwoOf これですね
あ,できないかもしれないです.これだと,ずっとPositionが0になるのか…
実行してみましたが確かに毎回Positionがリセットされてしまいますね
liftIOを試してみましたがビルドエラーになってしまいました
    • Could not deduce (MonadIO m) arising from a use of ‘liftIO’
      from the context: MonadThrow m
        bound by the type signature for:
                   conduitGet3 :: forall (m :: * -> *) o.
                                  MonadThrow m =>
                                  IO.Handle -> Get o -> ConduitT BS.ByteString (o, Integer) m ()
        at app/Main.hs:49:1-93
      Possible fix:
        add (MonadIO m) to the context of
          the type signature for:
            conduitGet3 :: forall (m :: * -> *) o.
                           MonadThrow m =>
                           IO.Handle -> Get o -> ConduitT BS.ByteString (o, Integer) m ()
    • In a stmt of a 'do' block: i <- liftIO (IO.hTell h)
      In the expression:
        do i <- liftIO (IO.hTell h)
           yield (x, i)
           if BS.null rest then awaitNE >>= start else start rest
      In an equation for ‘result’:
          result (Done x rest)
            = do i <- liftIO (IO.hTell h)
                 yield (x, i)
                 if BS.null rest then awaitNE >>= start else start rest
   |
75 |         i <- liftIO (IO.hTell h)
   |              ^^^^^^^^^^^^^^^^^^^
あ、 conduitGet3 :: (MonadThrow m, MonadIO m) => IO.Handle -> Get o -> ConduitT BS.ByteString (o, Integer) m () にしたらビルドできました!
ビルドはできたのですが出力結果が
(Nil,3)
(Bool True,3)
(Bool False,3)

となってしまいました :sob:

本当は
(Nil,0)
(Bool True,1)
(Bool False,2)

となってほしいのですが…
遅延評価だから全部最後の状態のを見てしまっているということなのでしょうか…?
ちなみに echo -en "\x00\x01\x02" で作成したファイルを読ませていて、0x00がNil、0x01がBool True、0x02がBool Falseとなっております
いえ実はその問題は指摘しようと思ってたのですが, sourceFile は256kBごとによみとるため, hTellで取り出せるのは読み込んだ後(パースされた文字列のポジションではなく)の値になります.今回の例では最初に最後までHandleが読み込まれてしまった結果そうなったのだと思います
そ、そんな…この機能を実現するのはConduitではできないのでしょうか…?
そもそも僕のこのやり方がおかしいのでしょうか…?
HandleのPositionを使わない場合,こんな感じで書けます.
https://gist.github.com/mizunashi-mana/9e8f9703cf21698a4312ba19874bcd48

HandleのPositionを使う場合上の問題があるため,結局Position修正を行わなければならないためhTellを使うよりこっちのほうがいいかもしれません.
うーん,おそらくですが conduitGet2 を流用するのは無理なんじゃないかなと思いますね.やるとしたらConduitに新しいAPIを足さなきゃいけない気がします( らへん低下層のデータ型を操作するようなAPIを足せば色々できると思いますね.今回の場合に使えるAPIはあってもおかしくないと思うんですが,探した感じ見つかりませんでした)
なるほど、内部でカウンタをもつということですね!
なるほど、流用は難しいですか…
あー…!完全に後出しジャンケンなのですが、 echo -en "\x00\x01\x02\x08\x01" こんな感じで \x08\x01 の2つで UInt8 1 のようなデータもあるんです…
これ、先程の内部でカウンタ持つやり方だとできないですかね…?
あ、bytesReadで何バイト読んだかわかるからそれをカウンタに足せばいけますかね!?
それさっき思いついて実装してます.ちょっと応用すれば簡単に流用できましたね…なるほど
conduitGet4 :: MonadThrow m => Get o -> ConduitT BS.ByteString (o, Int) m ()
conduitGet4 g = conduitGet2 ((,) <$> g <*> bytesRead) .| conduitPosFix 0
  where
    conduitPosFix !p = await >>= \case
      Nothing -> pure ()
      Just (x, l) -> do
        yield (x, p)
        conduitPosFix $ p + l

こんな単純にできそうですね…
おお!
あら、ビルドエラーになってしまいました…
error: parse error on input ‘case’
   |
84 |     conduitPosFix !p = await >>= \case
   |
いろいろ試してみましたがまだHaskellの文法をきちんと理解できておらず↑のビルドエラーを直せませんでした…
あ! -XLambdaCase が必要なんですね!失礼しました!
あすいません、 {-# LANGUAGE LambdaCase #-} が必要です
    Variable not in scope:
      conduitPosFix :: Integer -> ConduitM (o, Int) (o, Int) m ()
   |
82 | conduitGet4 g = conduitGet2 ((,) <$> g <*> bytesRead) .| conduitPosFix 0
   |                                                          ^^^^^^^^^^^^^

whereでconduitPosFixが定義されているのにエラーになってしまう…