タプルをファンクターとして定義することによって引き起こされる問題

非常に驚くべきことです(突然です!)、しかし、GHCのペアタプルはファンクターです。 ファンクターにはパラメーターが1つだけ必要で、ペアには2つのパラメーターがあるため、これを理解するのは困難です。 標準GHCライブラリの開発者がこのような抽象化を提供する方法を賞賛することはできますが、結果のソリューションは依然として失敗として認識されるべきであるように思われます。



そもそも直感的に理解できない。 GHCツールを使用せずに、式(1+) `fmap` (2, 3)



を手動で計算してみてください。 その後、 ghci



などで結果を確認しghci



。 多くの場合、手動計算の結果はシステムが生成した結果と一致しましたか? そして、それでもあなたの結果が一致した場合、私はあなたがそれをどのように正確に行ったかについての良い説明を聞きたいと思います。



インクリメンタルタイプのIncable



クラスを定義するとします。



 class Incable t where inc :: t -> t
      
      





もちろん、任意の数値型に対してこのクラスのインスタンスを定義する方が簡単で論理的です。



 instance (Num t) => Incable t where inc = (1+)
      
      





ただし、このような定義では、 UndecidableInstances



言語拡張機能を有効にして、状況を完全に混乱させる必要があります。 したがって、 Num



クラスの最も基本的な代表であるInteger



型とDouble



型の定義のみに限定します。



 instance Incable Integer where inc = (1+) instance Incable Double where inc = (1+)
      
      





明らかに、どのファンクターでも、 Incable



クラスのインスタンスを定義できます。



 instance (Functor f, Incable t) => Incable (ft) where inc = fmap inc
      
      





これはかなり良い一般的な定義です。 リスト、配列、 Maybe



構造、およびFunctor



クラスインスタンスが定義されているさらに多くのタイプを操作できます。 このすべての多様性の中に、増分値のペアもあります。 ただし、(ファンクターとしてのペアの現在の動作について説得力のある説明を見つけたとしても)ペアの両方の値が一度に増加することが望ましい場合があります。 次のようにペアのIncable



クラスのインスタンスを定義することは素晴らしいことです。



 instance (Incable t1, Incable t2) => Incable (t1, t2) where inc (x1, x2) = (inc x1, inc x2)
      
      





悲しいかな、これは不可能です。 もちろん、このフラグメントのコンパイルは成功しますが、この定義を使用しようとすると、インスタンスの重複に関するメッセージが表示されます。



 ghci> inc (1, 2) <interactive>:105:1: Overlapping instances for Incable (t0, t1) arising from a use of `inc' Matching instances: instance (Functor f, Incable t) => Incable (ft) -- Defined at src/Main.hs:16:10 instance (Incable t1, Incable t2) => Incable (t1, t2) -- Defined at src/Main.hs:18:10 In the expression: inc (1, 2) In an equation for `it': it = inc (1, 2)
      
      





すべてのファンクターに対してIncable



クラスのインスタンスを定義するIncable



、ペアに対してもすぐに定義しました。 これに加えて、GHCでは、インポート中にクラスのインスタンスの定義を非表示にすることはできません。 つまり、ファンクターとして必要ないカップルの行動は、本当に必要な機能への負荷になります。



3つなどの要素が3つ以上あるタプルを考えると、状況はさらに複雑になります。 inc (1.0, 2, 3)



ようなものを計算する必要がある場合はどうしますか? ここでは、間違いなく問題はないはずです-トリプルはファンクターとして定義されていません。つまり、 Incable



なように、それらにIncable



クラスのインスタンスを実装できるということです。



 instance (Incable t1, Incable t2, Incable t3) => Incable (t1, t2, t3) where inc (x1, x2, x3) = (inc x1, inc x2, inc x3)
      
      





繰り返しますが、コンパイルは成功し、実行するとエラーが発生します。



 ghci> inc (1.0, 2, 3) <interactive>:14:1: Overlapping instances for Incable (t0, t1, t2) arising from a use of `inc' Matching instances: instance (Functor f, Incable t) => Incable (ft) -- Defined at src/Main.hs:18:10 instance (Incable t1, Incable t2, Incable t3) => Incable (t1, t2, t3) -- Defined at src/Main.hs:20:10 In the expression: inc (1.0, 2, 3) In an equation for `it': it = inc (1.0, 2, 3)
      
      





さて、トリプルも個別に定義することはできません-ペアのように、それらは一種のファンクターと見なされます。 たぶん、増分トリプルの定義は必要ありませんか? どんなに! この定義を削除して、再試行します。



 ghci> inc (1.0, 2, 3) <interactive>:12:1: No instance for (Functor ((,,) t0 t1)) arising from a use of `inc' Possible fix: add an instance declaration for (Functor ((,,) t0 t1)) In the expression: inc (1.0, 2, 3) In an equation for `it': it = inc (1.0, 2, 3)
      
      





ここで、トリプルをファンクターとして定義するように求められます。 必要に応じてトリプルのFunctor



クラスのインスタンスを定義できますか?



 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (fmap f x1, fmap f x2, fmap f x3)
      
      





このような定義をコンパイルすることもできません。



 [1 of 1] Compiling Main ( src/Main.hs, interpreted ) src/Main.hs:19:36: Couldn't match expected type `b' with actual type `f0 b' `b' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the return type of a call of `fmap' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) src/Main.hs:19:43: Couldn't match expected type `a' with actual type `f0 a' `a' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the second argument of `fmap', namely `x3' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) Failed, modules loaded: none.
      
      





最後のチャンスとして、ペアとの類推によってファンクターとして3を定義してみましょう。



 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (x1, x2, fmap f x3)
      
      





残念ながら、この定義は前の定義と同じ理由でコンパイルされません。



 [1 of 1] Compiling Main ( src/Main.hs, interpreted ) src/Main.hs:19:36: Couldn't match expected type `b' with actual type `f0 b' `b' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the return type of a call of `fmap' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) src/Main.hs:19:43: Couldn't match expected type `a' with actual type `f0 a' `a' is a rigid type variable bound by the type signature for fmap :: (a -> b) -> (t1, t2, a) -> (t1, t2, b) at src/Main.hs:19:5 In the second argument of `fmap', namely `x3' In the expression: fmap f x3 In the expression: (x1, x2, fmap f x3) Failed, modules loaded: none.
      
      





議論



一般的に言えば、ファンクターとしてのタプルペアの定義は、あまり良い解決策ではなかったと言えます。 場合によっては、これがファンクター抽象化の使用を妨げる深刻な問題を引き起こします。 2つの基本的な質問に答えることは非常に重要です。 まず、なぜペアのFunctor



クラスのインスタンスを定義する必要があったのですか? 第二に、3つ以上の要素を持つタプル(たとえば、トリプル)に対して同様の定義をどのように行うことができますか(まったく可能ですか)。



さらに、ペアをさまざまなファンクターとして定義することは完全に論理的ではありません。 ファンクターは常にパラメーターを1つだけ持つ型であることが一般に受け入れられています。 同時に、最も単純なタプル(ペア)でもすでに2つのパラメーターがあります。 同時に、ペアのFunctor



クラスのインスタンスの定義で、2つではなく1つのパラメーターのみが示されている理由は完全に理解不能です。



もちろん、同じタイプのタプル、つまりタイプ(t, t)



(t, t, t)



などのタプルに対してのみFunctor



クラスのインスタンスを定義するように制限することは、非常に論理的です(そして正当化されることさえあります(t, t, t)



。タプルはリストと違いはありませんが、重要な機能は、コンパイルの段階で要素の数を設定するのが難しいことです。 このようなタイプは、たとえば、整数のペアとトリプルを表すことができますが、これらは同時に同じ方法で処理する必要があります。 一般的なケースでは、 マルチパラメーターファンクターを使用する方が適切です。



マルチパラメータファンクタは、通常のファンクタの一種の一般化ですが、いくつかのパラメータを持つ型のためのものです。 たとえば、2パラメータファンクタは次のように定義できます。



 class Functor2 f where fmap2 :: (a1 -> b1) -> (a2 -> b2) -> f a1 a2 -> f b1 b2
      
      





ここでのロジックは、1つの関数ではなく2つの関数を使用し、それぞれがパラメーターファンクターに適用されるというものです。 この場合、ペアは2パラメーターファンネルのインスタンスとして定義でき、3つは3パラメーターファンクターのインスタンスとして定義できます。 ここで、この定義を受け入れれば、タプルの各要素がどのように処理されるかを明確に想像し、必要に応じてこの動作を変更することさえできます。 確かに、そのような抽象化がどれほど役立つか、タプルを除くこれらのクラスに他の有用な型があるかどうかは不明です。



これは、私たちが取得したかったものではない、と言うだけです。 実際、ファンクタを均質計算の優れたモデルとして使用することから始めました。 この場合、ペアの要素は異なる型を持ちますが、それらはすべて同じクラスに属し(増分型、より抽象的なアプローチで、一般に数値型です)、使用する関数は多態性です。 したがって、コンパイラーに主題領域のこれらの機能を伝えることができれば、通常のファンクターとうまくやっていくことができます。 残念ながら、現時点では、これがどのように行われるかはまったく想像できません。



結論



議論の結果に基づいて(私の問題にすぐに応答してくれたすべての読者に感謝します)、特定の結果を定式化することができました。 著者として、私は記事が不完全さの印象を残さないように、記事をわずかに修正することにしました。



まず、トリプルをファンクターとして定義するときに犯した非常に厄介な間違いについて謝罪したいと思います。 もちろん、正しいコードは次のようになります。



 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (x1, x2, f x3)
      
      





このコードはコンパイルされ、期待どおりに動作します-変更は最後の要素のみに関係します。 同様に、私たちの場合、次の形式のコードを実装したいと思います。



 instance Functor ((,,) t1 t2) where fmap f (x1, x2, x3) = (f x1, f x2, f x3)
      
      





しかし、わずかに異なる理由のために、それはコンパイルしません:



  [1 of 1] Mainのコンパイル(src / Main.hs、解釈)

 src / Main.hs:19:28:
    タイプ 'b' with` t2 'と一致できませんでした
       「b」は、
          型シグネチャ
             fmap ::(a-> b)->(t1、t2、a)->(t1、t2、b)
           src / Main.hsで:19:5
       「t2」は、
            src / Main.hsでのインスタンス宣言:18:10
    期待されるタイプ:t1
      実際のタイプ:b
     `f 'の呼び出しの戻り型
    式:f x1
    式では:(f x1、f x2、f x3)

 src / Main.hs:19:30:
    タイプ 't1' with` t2 'と一致できませんでした
       「t1」は、
            src / Main.hsでのインスタンス宣言:18:10
       「t2」は、
            src / Main.hsでのインスタンス宣言:18:10
    期待されるタイプ:a
      実際のタイプ:t1
     `f 'の最初の引数、つまり` x1'
    式:f x1
    式では:(f x1、f x2、f x3)

 src / Main.hs:19:34:
    タイプ 'a' with` t2 'と一致できませんでした
       「a」は、
          型シグネチャ
             fmap ::(a-> b)->(t1、t2、a)->(t1、t2、b)
           src / Main.hsで:19:5
       「t2」は、
            src / Main.hsでのインスタンス宣言:18:10
    期待されるタイプ:t2
      実際のタイプ:b
     `f 'の呼び出しの戻り型
    式:f x2
    式では:(f x1、f x2、f x3)
失敗、ロードされたモジュール:なし。


カップルトリックがどのように行われたかは明らかです。 実際、部分的なアプリケーションによって、任意のマルチパラメータータイプを1つのパラメーターに変換してから、そのためのファンクターを実装できます。



ただし、システムライブラリレベルでペアをファンクターとして定義することは悪い決定であると私はまだ主張します。 これは、他のマルチパラメータータイプ(高次のタプル、マップなど)の場合、 Functor



クラスのインスタンスがシステムライブラリで提供されないという事実によってもサポートされます。 私は、開発者が単に「インプレース」のどこかにペアのファンクターの振る舞いを必要としていたのではないかと疑っています。 このバージョンを確認するには、ペアをファンクターとして定義せずにシステムライブラリを再構築するのが最も簡単ですが、そのような実験の時間はありません。



明らかに、システムレベルでは、ペアの定義のみをbifunctorとして残し、このアプローチを高次のファンクターに一般化することも合理的です 。 もちろん、部分的な適用によって、型の順序よりも低い順序のファンクターの簡略化された定義を取得する可能性が常にあります。 この場合、型の部分的な使用によって除外された構造要素は処理できなくなります。 マッピング関数を部分的に適用して同じ動作を記述する方が論理的です。最初の引数は同一のid



関数です。 グローバルに定義されたインスタンスとは異なり、そのような縮小表示機能は、対応するモジュールをインポートするときに必要に応じて簡単に非表示にできます。



All Articles