haskell-jp / questions #99 at 2021-12-23 19:35:00 +0900

Sum Typeに対するLensのフィールドを自動生成する方法について質問です。
(多分日本語を含むのでバイナリ扱いされている)スニペットに詳細を書いてあるように、

deriveFieldsForSumType ''HasBase ''ToyExpr

のように書いたら、

instance HasBase ToyExpr Text where
  base = lens getter setter
    where getter (ToyExprToyInt x) = view base x
          getter (ToyExprToyStr x) = view base x

          setter (ToyExprToyInt x) y = toToyExpr $ set base y x
          setter (ToyExprToyStr x) y = toToyExpr $ set base y x

のようなコードが自動生成されると嬉しいなと思っています。

軽く探した所そういうライブラリは見当たらなかったので、
自分で書こうとも思っていますが、
Template Haskellはそんなに得意では無いので、
既存のライブラリがあったり、
そもそも他の方法を使えばボイラープレート書かなくても良いなどという指摘があれば欲しいと思っています。
こちら、概ね自分で書けてきた感じがします。
求めている機能はジェネリクスやTHで実装できるとは思いますが、baseに相当する値が入っていることがあらかじめ保証された型を使うのが定石だと思います。data ExprF a = Lit Literal | App a aのようなFunctorとdata Cofree f a = a :< f (Cofree f a)を使ってASTを定義すれば、Cofree ExprF Textは各ノードに必ずTextが入っていることが保証され、さらに型などのアノテーションに応用することも容易です
前提知識が足りていないからかちゃんと物事を理解しているか自信が全く無いのですが、
それは一部のツリーには共通したフィールドがあるけど、
他のツリーではそうでもないという場合でも応用可能ですか?

業務の問題の無いコード部分許可とって持って来ればよかった気がしますが、
例えば、
MyStrを更に分割して、UTF-8, UTF-16, UTF-32のそれぞれの文字列型のSum Typeにします。
そこでMyStrに対しては文字列に共通する、
文字列の容量のlengthを共通のlensのアクセサとしてアクセスできるようにします。
そしてMyExprからは一発でlengthは取れないが、
MyStrまで分解したらそれ以上のパターンマッチ無しでアクセサでアクセスできるようにしたいという感じです。
こういった場合でもその定石は使えるものなのでしょうか。
そのlengthのアクセサを使って何らかの値をセットしたとき、どのような挙動をしますか?
単純にパターンマッチしてセットを行ったデータをセットしたのと同じで、値が更新されたデータが帰ってきます
すみません、理解できないです。lengthのアクセサを使って"hello, world"に4をセットしたらどうなりますか?
…あー、確かにおかしな話をしてしまいました
文字列バリアントに無理に共通のデータフィールドを考えて長さぐらいしか思いついてなかったので無茶なことを言っていました
実際、プロダクトコードでは構築時にはそれぞれデータ型を作って、Sum Typeをたくさん含むツリーを構築し終わったあとに、中間レイヤーでサクッと、この例ではbaseだけを出して文字列リストとして見るとか、基本的に読み出し専用で使うことが多いです
コメントが全く理解できなかったのでコードだけ見るとだいたい何をしたいのか想像できました。ToyExprにHasBaseを持たないコンストラクタを追加した場合、Lensは定義できなくなります(Traversalにはできます)。そして、HasBaseのインスタンスがあるかどうかは、THはもちろんそのモジュールをコンパイルしている段階では通常知りえない情報なので、そのTraversalはToyExprに対するパターンマッチによって実装すべきだと思います。`ContainsBase :: Type -> Bool`なる型族を定義すればTraversalの対象になるかどうか決められるため、ジェネリクスによる導出も可能になりますが、ほとんどの場合は割りに合わないでしょう
やっぱり業務とちょっと違う技術的に近い例え話を行うのは困難だったことを感じています…
ToyExprにHasBaseを持たないコンストラクタを追加するのではなく、Sum TypeであるToyExprの下位のToyStrが更にToyStrUtf8とかのSum Typeであるとかの想定で、ToyExprは共通するlengthフィールドを持たないけれどToyStrは共通するlengthフィールドを持つ場合部分的に統一アクセスは出来るかみたいな事を言っていました。
「他のツリーではそうでもない」というのはそういう意味(HasBaseを持たないコンストラクタ)ではなかったのですか?lengthはそもそもフィールドではないし、なぜToyExprに言及する必要があるのかもわかりません。
はい、違います
baseは全体にあるけど、lengthは一部のツリーに共通するフィールドだけど全部のツリーが持っているフィールドではない、という意図でした
説明が下手で申し訳ありません
lengthの不自然さについては、例を頑張って考えただけなので仮にlengthを作っただけなので単に文字列バリアントだけに共通するフィールドがあると捉えてもらえると嬉しいです
説明については、「文字列バリアント」「ツリー」 「下位」「統一アクセス」のような未定義語を減らしたり、一つの問題に焦点を当てる(この場合、lensやMyExprの話を除外し、MyStrにフォーカスする)と分かりやすくなると思います。あくまで私の想像ですが、 data Hoge = FromA A | FromB B | FromC C のようなバリアント型が与えられており、A、B、Cが class HasFoo a where getFoo :: Lens' a Fooのような型クラスのインスタンスであるとき、 HasFoo Hoge のインスタンスを導出したい、ということですか?
はい、その通りです。
それ自体は自分でTHを書いて概ね実現できました。
ボイラープレートの一部削減は実現できました。
会社の許可などが取れたらOSSとして公開したいと思っています。

その上でふみあきさんの最初の返信のExprFの話が私の知識不足からかよく分からなかったため、質問を続行していたという形になります。
なるほど、ようやく理解できました。面白い問題を発見しましたね。これはジェネリクスを使って簡潔に書けるので、実装例を紹介します。この例ではdata Numeral = NumInt Int | NumDouble Double の中身がNumおよびShowであるという前提をHasCommonalityで表しています。viewsCommonやoverCommonによって、中身がIntの場合もDoubleのときもshowしたり+1したりできます
なるほど、ジェネリクスは正直あんまり分かってないので実装はよく分からないのですが、かなり簡潔に書けるのですね。
こちらのプロダクトでは通常のフィールドアクセスにLensを使っているのでLensの関数合成が便利なことと、THのインスタンス生成では型推論の補助をせずともアクセス出来る点を考えると、どちらの方法にも利点欠点があるなあと感じました。
ジェネリクス版での実装を示していただいてありがとうございます。
traverseCommon自体はLensではありませんが、`\f -> traverseCommon @IsText (Data.Text.Lens.unpacked f)` のように他のLensと合成すればLensになります。型推論の補助というのはTypeApplicationのことを言っているのでしょうか?どのクラスを利用するかはどこかで指定しないといけないと思いますが……
はい、TypeApplicationのことを言っています。
lensのインスタンス生成をする方法だとその生成時に確かに指定は必要ですが、生成した後のアクセサを利用するのに型引数は不要のため、その点で一長一短かなあと思った次第です。