web-dev-qa-db-ja.com

なぜ(またはなぜそうではない)存在型が関数型プログラミングの悪い習慣と見なされているのですか?

コードを一貫してリファクタリングして、存在型への依存を排除​​するために使用できるいくつかのテクニックは何ですか?通常、これらは、タイプの不要な構成を無効にするために使用されます。また、指定されたタイプについての最小限の知識で消費できるようにします(または私の理解です)。

誰かがコードでこれらへの依存を取り除く単純で一貫した方法を考え出しましたか?それでもいくつかの利点を維持していますか?または、少なくとも、変更に対処するために大幅なコードチャーンを必要とせずにそれらを削除できる抽象化をすり抜ける方法はありますか?

存在タイプについての詳細は here ( "勇気あるなら..")を読むことができます。

43
Petr Pudlák

実存型は、関数型プログラミングでは実際には悪い習慣とは見なされていません。多くの人が悪い習慣だと信じている existential typeclass antipattern は、存在の最も一般的に引用されている用途の1つだと思います。

このパターンは、多くの場合、すべて同じ型クラスを実装する異種の型付き要素のリストをどのように持つかという質問への回答として取り除かれます。たとえば、Showインスタンスを持つ値のリストが必要になる場合があります。

{-# LANGUAGE ExistentialTypes #-}

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

data AnyShape = forall x. Shape x => AnyShape x
instance Shape AnyShape where
    area (AnyShape x) = area x

example :: [AnyShape]
example = [AnyShape (Circle 1.0), AnyShape (Square 1.0)]

このようなコードの問題はこれです:

  1. AnyShapeで実行できる唯一の便利な操作は、その領域を取得することです。
  2. AnyShapeコンストラクターを使用して、形状タイプの1つをAnyShapeタイプに組み込む必要があります。

結局のところ、そのコードは、この短いコードでは得られないものを実際には得ていません。

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

example :: [Double]
example = [area (Circle 1.0), area (Square 1.0)]

マルチメソッドクラスの場合、Shapeのようなタイプクラスを使用する代わりに、「メソッドのレコード」エンコーディングを使用することで同じ効果を一般的に簡単に達成できます。フィールドがShapeタイプの「メソッド」。円と正方形をShapesに変換する関数を記述します。


しかし、それは存在型が問題であるという意味ではありません!たとえば、Rustの場合、それらには traitオブジェクト という機能があります。これは、特性(Rustのバージョンの型クラス)に対する存在型として人々がよく説明するものです。 Haskellのアンチパターンとは、Rustが悪い解決策を選んだということですか?いいえ、Haskellの世界での動機は、構文と利便性に関するものであり、原則に関するものではありません。

これを表すより数学的な方法は、上からのAnyShape型とDouble同型であることを指摘しています。それらの間の「ロスレス変換」です(まあ、浮動小数点精度のために保存します):

forward :: AnyShape -> Double
forward = area

backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))

つまり、厳密に言えば、どちらか一方を選択しても、力を獲得したり失うことはありません。つまり、使いやすさやパフォーマンスなど、他の要因に基づいて選択する必要があります。


また、存在タイプには、この異種リストの例以​​外にも用途があるため、それらを使用することをお勧めします。たとえば、HaskellのST型は、外部的には純粋であるが内部的にメモリ変換操作を使用する関数を記述できるため、コンパイル時の安全性を保証するために存在型に基づく手法を使用します。

一般的な答えは、一般的な答えはないということです。存在タイプの使用は、コンテキスト内でのみ判断できます。回答は、異なる言語で提供されている機能や構文によって異なる場合があります。

8
sacundim

私はHaskellにあまり詳しくないので、非学術的な関数型C#開発者として、質問の一般的な部分に答えようと思います。

いくつかの読み取りを行った後、次のことがわかりました。

  1. Javaワイルドカードは存在型に似ています。

    例によるScalaの存在型とJavaのワイルドカードの違い

  2. ワイルドカードはC#では完全に実装されていません。一般的な差異はサポートされていますが、呼び出しサイトの差異はサポートされていません。

    C#ジェネリック:ワイルドカード

  3. あなたは毎日この機能を必要としないかもしれませんが、あなたはそれを感じるでしょう(例えば、物事を機能させるために追加のタイプを導入する必要がある):

    C#汎用制約のワイルドカード

この情報に基づいて、存在タイプ/ワイルドカードは適切に実装されている場合に役立ち、それ自体には何の問題もありませんが、他の言語機能と同じように誤用される可能性があります。

2
Den