haskell-jp / questions #41

OOP 脳に洗脳されていると、二項演算子の左と右が同じ型でないといけないっていうこと自体がびっくりだろうからそれを言おうとしてるのかなあ、と。
うーん、文法的な理解に違いがあるように見えます…例えば「あなたにはプログラミングの才能がある」という言い方ができるからといって、「才能がある(という性質)とは、あなたのことである」と言うことはできないですよね。「あなた」を型、「才能がある」をインスタンス、「プログラミングの」を「ある型クラスの」と置き換えても同じことが言えます
意地悪に捉えればそういう見方もありますね…ただ色々と成り立ってないので、私は忘れたほうがいいという結論になりました…
まあ、いじわるというか、この部分についても「まぎらわしい」ので、ぶっちゃけ読む価値がないなあ、と^^;
型クラスは (種, 種, …, 型, 型, …) という組で、インスタンスはそれに対応する (型, 型, …, 値, 値, …) という組というイメージ(ただし Haskell では、インスタンスは型の組合せの部分で一意にならないといけない)
それはそのとおりです。なので、その文法的な問題は「Eq のインスタンス」のところには当てはまらないんじゃないかな、と思いました。ぶっちゃけ言いたいことはそれだけです^^;
「Eqのインスタンス」のように型クラスが明記されていれば、それは型に対する述語となるのでもちろん問題ないと思います。
要は例えば groupBy::(a->a->Bool)->[a]->[[a]] にいちいち最初の引数を渡すの面倒だから、この型なら必ずこの関数を使ってねっていうのを instance で決めておけば group :: Eq a=>[a]->[[a]] が使えるよ、っていう話ですよね。で、逆にそれじゃ困る人は毎回 groupBy に何か渡すしかない、っていう。
Scala や Coq の原理はこうなんですよね。まず、型を定義して、それを暗黙引数で型クラスとして使う方法。
newtype も一応使えます
newtype するのは一つの手ではありますが、皮をかぶせたり外したりしないといけないので、 groupBy が必要な状況ではあまり向いてないかなあ、と思ってあえて groupBy を例に出しました^^;
まあ、そういう状況でも newtype でくるんで外した方がラクだよね、っていう発想をするのが真の Haskeller なのかもしれませんね…^^;;
そうですね、横槍失礼しました :bow:
(foldl や foldr が中で、 Endo に包み込んで foldMap に扱わせてるのを見たりすると、たしかに包んで外す方がラクだという話はあるんだな、と思いました)
あ、今は coerce でわざわざ外したりくるんだりしなくとも変換出来たり
束縛・代入の話のときもそうでしたが、基本的な用語で意見が割れることがあり油断できない…
包むコンストラクタ・外すrunほげ のかわりに coerce / coerce を書くだけですよね^^;
少し前に https://haskell-jp.slack.com/archives/C5666B6BB/p1548483818293400 の話題がでましたけど、これなんかはまさに「型(コンストラクタ)に対して一個しかインスタンス宣言できない」がゆえの悩みだったりしますよね…
ところで questions らしく質問ですけど、標準 Haskell だと「instance 宣言自体の export/import をするかしないか」は選べなくて必ず export しちゃうし必ず import しちゃうんですけど、 GHC 拡張だとそこらへんコントロールできたりするでしょうか?
さぁ... そんな拡張はちょっと聞いたことがないですね... あったとして使われたら結構混乱しそうな気がします... :fearful:
意味がどうなるかがわからないのが確かに混乱しそうですね。ただ、こう、自分のモジュールで「もともと Show インスタンスじゃないんだけど、自分のデバッグのためだけにこのモジュールではこういう形で表示させたいのでそのためだけに Show インスタンスに入れておきたい(けどヨソのモジュールでは使ってほしくないので export はしたくない)みたいな話って、ありそうな気がするんですよね…
あと、標準の Show インスタンスの定義がきにいらないので自分版を使いたい、とか^^;
まあ、いずれも show にこだわることなく自分で文字列化関数を作ってそれを使えという話ではあるんですが…。
自分版で「オーバーライド(?)」できてしまうと、別モジュールのライブラリの奥の方で print してるような関数ではたしてどっちの show が使われるのか、ワクワクしますねw
(おそらく最初の Show a=>制約を見たときにセットされた辞書がたらいまわしにされると思うんですが…)
完全な自由を与えるわけではないんですが、特定の範囲でメソッド(が入った辞書)を差し替えることができる、という意味では去年のHaskell Symposiumで発表された https://icfp18.sigplan.org/event/haskellsymp-2018-papers-coherent-explicit-dictionary-application-for-haskell が近いような気がします。
あぁっ、以前は無料でPDFがダウンロードできたのに今はできない? :disappointed:
実装だけ見ても難しいかとは思いますが、 https://github.com/mrBliss/ghc これがそのプロトタイプです。
Sample.hs を見ていました。 visible type application のような感じで、二重丸括弧で ((辞書用の型のデータ)) と書けば辞書が渡せる、っていう感じなんですかね。
自分で分かっていないことを言うのですが、 backpack で似たようなことはできないのでしょうか。
型や関数まとめるのにbackpackのsignatureを使っている場合であれば、「このunitではこのsignatureに該当するmoduleはこんなmoduleです」、と自由に宣言できるので、できると思います。
えーと、 backpack というものを知らないのでいまググった程度の知識ですが、それはつまり「型クラスを使うというコースを捨てて、 backpack のモジュール化機構を使いましょう」っていう解決策ですかね?
こう、結局「C でヘッダファイルだけ公開してコンパイルさせておいて、実際にリンクするライブラリは切り替え可能」って言ってるのに近い感じ(っていうかそのもの?)なので、 C ってしぶとく生き残る強さがあるよなーと思ってしまいます^^;
上記と関連するのですが、現状 instance 宣言は取捨選択できないので、二重定義を避けるために「どのモジュールに instance 宣言を置くべきか」というのが悩ましい気がしています。自分で便利な型クラスを作ったのでそれに Int, Double …などのインスタンスを書きたい場合、逆に自分で便利な型を作ったのでそれを Num, Show, Eq …などのインスタンスにしたい場合、その両方の場合がありうるので、「型クラスの定義があるモジュール側にインスタンスを書く」のも「型の定義があるモジュール側にインスタンスを書く」のもなんか違う気がしています。なんかベストプラクティスとかあるんでしょうかね?(既存ライブラリの組み合わせで実際に問題がおきたりはしていないのでしょうか?)
インスタンス宣言の export/import する方法は現状ないはずです(少なくとも一般的な方法は)
そして、おっしゃる通り backpack も型クラスとは別の機能であってます :ok_hand:
「自分で便利な型クラスを作ったのでそれに Int, Double …などのインスタンスを書きたい場合」「型クラスの定義があるモジュール側にインスタンスを書く」
で、
「自分で便利な型を作ったのでそれを Num, Show, Eq …などのインスタンスにしたい場合」「「型の定義があるモジュール側にインスタンスを書く」
でよいのではないでしょうか。
というか、そうじゃなければorphan-instanceの警告でますし...
この辺は僕も悩んでて正解を知りたいのですが,標準的な答えとしては「型クラスの定義があるモジュールにインスタンス宣言がないということは実装がないということが標準(型クラス提供者が考えるデファクト)である」/「新たな型定義があるモジュールにインスタンス宣言がないということは,その型に実装がないことが標準(型提供者が考えるデファクト)である」ということだと思っています.なので,デファクトから外れる場合 newtype なりで自分自身の考えるインスタンスを追加するべきというのが現状のHaskellの主流ではないでしょうか?( orphan instance の警告が出るのがそれを示していると思います.この辺は shadowing も事情が似てる気がします)
この思想自体は理解できますが,実際に使う場合インスタンスを柔軟に切り替えたいことはありますし,保守性の面で orphan instance を作らなければいけない場合はあります( base-orphans などが有名な例だと思います).
また, parsers ライブラリは orphan instance を避けるため attoparsec / parsec に依存してるわけですがおかげで megaparsec から批判が飛んでくるといった事態を引き起こしています.
ただ, orphan instance はかなり注意深く管理しないと壊れる可能性も高い (instance宣言の明示的なexport/import はできないので,1つの orphan instance のせいでモジュール全てが使えなくなるといったことも起きる) ので,結構リスクが高いです.
なので,よほどのことがない限り orphan instance を使わず newtype によって新しいインスタンス宣言は作っていくべき,ただ注意深く管理しリスクを許容するなら使うべき時はあるかもしれないというのが,個人的に知る限りのベストプラクティスだとは思いますね(ただあまり納得はできていないのですが)
多分、orphan instanceを提供する目的のライブラリーとアプリケーション以外はorphan instanceを作らず、個々のアプリケーションでのみそれらを使う(または自分でorphan instanceを定義する)が正解じゃないかと思っています。
ライブラリA(クラスXを提供していて、既存の有名どころのデータ型のインスタンスは一通り提供している)と、
ライブラリB(データ型Yを提供していて、既存の有名どころのクラスへのインスタンスは一通り提供している)があるとしたときに、
ライブラリAとBを組み合わせた時のYのXへのインスタンス宣言はとりあえずその状態ではライブラリ利用者側で書くしかないわけですが、一方これらのライブラリの製作者側の立ち位置に立った時にお互い相手の存在を認知したときに自分(のライブラリの次期バージョン)でインスタンス宣言を書きたくならないのかなあ、というところが疑問のポイントでした。
第三のライブラリとして orphan instance のみを提供するライブラリが出現する、というのが流れなんですかねえ、この場合は。
@koyanagis has joined the channel
一方これらのライブラリの製作者側の立ち位置に立った時にお互い相手の存在を認知したときに自分(のライブラリの次期バージョン)でインスタンス宣言を書きたくならないのかなあ、というところが疑問のポイントでした。
よっぽどの需要がない限り書く必要がないと考えるんじゃないかな。。mizunashiさんがいうとおりYのデータ型をnewtypeでくるんで、そのくるんだ型でインスタンス定義すればいいだけだし。
僕がXを提供しているライブラリの製作者ならそう考えるね。
あかんやつや。しかし aws-lambda-haskell-runtime も相当やっつけ感があるし、これがマイナ言語の悲哀か。
第三のライブラリとして orphan instance のみを提供するライブラリが出現する、というのが流れなんですかねえ、この場合は。
だと思いますよ。現にそういうライブラリーはたくさんありますし。
Parsecに関する質問です。
MarkdownのOrderedListのようなものをparsecを用いて解析しようとしています。
Markdownでは - (ダッシュ)を用いてUnorderedListを表現しますが、今回は文章の前のwhitespaceの数がその代わりとなっています。
例:Markdownでは
- One
- Two
- Three
と表現するところを
One
Two
Three
と表現します。(見えにくいと思うけど、各行にはそれぞれ最初スペースがあります。。)
つまり文章の前にwhitespaceがあればリストであると判断することができます。
またwhitespaceの数が増えた場合、新たなリストが始まったと判断し、解析を始める必要があります。
問題はネスト化されたリストを解析する際に、それを正しく解析できていません。
https://gist.github.com/HirotoShioi/ddeaccf09b7545f64883019ea4b94c55
parseList exampleList
を実行すると (Right expecting)を返すようにしたいのですが、どこに問題があるのでしょうか。
僕はbulletPointParserに問題があるんじゃないかと思っています。
それを正しく解析できていません。
:thinking_face: どんな結果になるんでしょうか?
ParserErrorが返ってくる?それとも無限ループにはまる?
どちらにしてもできることとして、 traceShowIdで怪しいところにデバッグコードを挟んでみるのもよいんじゃないかと
具体的に,パーサに則ってパースしてみるといいと思います. bulletPointParser が間違っているというのはあっていて,具体的にはそれぞれの要素で lookAhead に入る前に空白を replicateM_ で消費してる数に注目してみるのが良い気がします.