12

関数型言語でデータ構造を設計する場合、次の 2 つのオプションがあります。

  1. それらのコンストラクターとパターン マッチを公開します。
  2. それらのコンストラクターを非表示にし、より高レベルの関数を使用してデータ構造を調べます。

どのような場合に、何が適切ですか?

パターン マッチングを使用すると、コードをより読みやすく単純にすることができます。一方、データ型の定義で何かを変更する必要がある場合は、パターン マッチ (または構築) するすべての場所を更新する必要があります。


私はしばらくの間、この質問を自分自身に尋ねてきました。単純なデータ構造 (またはtypeエイリアス) から始めることがよくありますが、コンストラクター + パターン マッチングが最も簡単なアプローチであり、クリーンで読みやすいコードが生成されるようです。しかし、後で事態はさらに複雑になり、データ型の定義を変更し、コードの大部分をリファクタリングする必要があります。

4

5 に答える 5

10

私にとって重要な要素は、次の質問に対する答えです。

データ型の構造は外の世界に関連していますか?

たとえば、リスト データ型の内部構造は、外部の世界と非常に関連性があります。これは、消費者に公開するのに非常に役立つ帰納的な構造を持っています。これは、リストの構造の帰納によって進む関数を構築するためです。リストが有限である場合、これらの関数は終了することが保証されます。また、このように関数を定義すると、ここでも帰納法によって、関数に関するプロパティを簡単に提供できます。

対照的に、Setデータ型を抽象化するのが最善です。containers内部的には、パッケージ内のツリーとして実装されています。ただし、配列を使用して実装するか、(機能的な設定でより便利に) 構造がわずかに異なり、さまざまな不変条件 (バランスまたはアンバランス、分岐係数など) を尊重するツリーを使用して実装することもできます。ちなみに、コンストラクターが型を介して既に適用している不変条件を超えて不変条件を適用する必要があるため、データ型を具象にすることはできません。

リストの例とセットの例の本質的な違いは、Setデータ型がで可能な操作にのみ関連することSetです。標準ライブラリはすでにリストに作用する多くの関数を提供しているため、リストは関連していますが、さらにそれらの構造関連しています。

補足として、実際にはリストの帰納的構造 (終了と動作を簡単に推論できる関数を作成する上で非常に基本的) が、リストを消費する 2 つの関数によって抽象的に捉えられていることに反対する人もいるかもしれません:foldrfoldl. これら 2 つの基本的なリスト演算子があると、ほとんどの関数はリストの構造を検査する必要がまったくないため、リストを抽象化しすぎる可能性があると主張できます。Traversableこの引数は、すべての構造体、すべての構造体など、他の多くの同様の構造に一般化されますFoldable。ただし、リストで考えられるすべての再帰パターンをキャプチャすることはほとんど不可能であり、実際、多くの関数はまったく再帰的ではありません。foldrとだけが与えられfoldlheadたとえば、非常に面倒ですが、まだ可能です。

head xs = fromJust $ foldl (\b x -> maybe (Just x) Just b) Nothing xs

リストの内部構造だけを公開する方がはるかによいでしょう。

最後のポイントの 1 つは、データ型の実際の表現が外部の世界と関係がない場合があることです。これは、ある種の最適化されたものであり、正規の表現ではない可能性がある、または単一の「正規の」表現がないなどの理由によるものです。このような場合、データ型を抽象化したままにし、データ型の「ビュー」を提供する必要があります。これにより、パターン マッチングが可能な具体的な表現が提供されます。

Complex1 つの例は、複素数のデータ型を定義する場合です。この場合、デカルト形式と極形式の両方を標準と見なすことができます。この場合、Complex抽象的に保ちますが、2 つのビュー、つまり関数polarとをエクスポートします。これらのビューcartesianはそれぞれ、長さと角度のペア、またはデカルト平面の座標を返します。

于 2012-09-06T15:37:28.190 に答える
7

ルールは非常に単純です。実際のコンストラクターを使用して間違った値を簡単に作成できる場合は、それらを直接使用することを許可せず、代わりにスマート コンストラクターを提供します。Mapこれは、やのようないくつかのデータ構造がたどるパスでありSet、間違いやすいものです。

次に、タイプがそれをまったく許可しないか、ボトムを導入する必要があるため、一貫性のない/間違った値を構築することが不可能または困難なタイプがあります。長さインデックス付きリスト型 (一般に と呼ばれるVec) とほとんどのモナドがその例です。

最終的にこれはあなた自身の決定です。ユーザーの視点に立って、利便性と安全性のトレードオフを考えてください。トレードオフがない場合は、常にコンストラクターを公開してください。そうしないと、ライブラリのユーザーは不必要な不透明さであなたを嫌うでしょう.

于 2012-09-06T15:30:04.400 に答える
5

データ型が ( のような) 単純な目的を果たし、データ コンストラクターを介して値を直接Maybe a構築することによって、データ型に関する (明示的または暗黙的な) 仮定に違反できない場合は、コンストラクターを公開します。

一方、データ型がより複雑 (バランスの取れたツリーなど) である場合や、内部表現が変更される可能性がある場合は、通常、コンストラクターを非表示にします。パッケージを使用する場合、非内部モジュールによって公開されるインターフェースは、指定されたデータ型で使用するのに「安全」でなければならないという暗黙のルールがあります。バランスの取れたツリーの例を考えると、データ コンストラクターを公開すると、(偶然にも) 不均衡なツリーを構築できるため、ツリーなどを検索するための想定される実行時の保証に違反する可能性があります。

于 2012-09-06T15:26:06.213 に答える
3

正規の定義と表現で値を表すために型が使用され (多くの数学的オブジェクトがこのカテゴリに分類されます)、その型を使用て「無効な」値を構築できない場合は、コンストラクターを公開する必要があります。

たとえば、2 次元の点のようなものを独自の型 (newtype を含む) で表現している場合は、コンストラクターを公開することもできます。現実には、このデータ型の変更は 2 次元の点の表現方法の変更ではなく、2 次元の点を使用する必要性の変化になります (おそらく 3 次元空間に一般化しているのかもしれません。レイヤーの概念を追加するなど)、何をするにしても、このタイプの値を使用するコードの部分で注意が必要になることはほぼ確実です。[1]

アプリケーションまたはフィールドに固有のものを表す複雑な型は、同様の操作をサポートし続けながら、表現が変更される可能性が非常に高くなります。したがって、内部構造ではなく、操作に応じて他のモジュールのみが必要です。したがって、コンストラクターを公開しないでください。

他のタイプは、正規表現ではなく正規定義で物事を表します。マップとセットに期待されるプロパティは誰もが知っていますが、それらのプロパティをサポートする値を表すさまざまな方法があります。したがって、特定の表現ではなく、サポートする操作に応じて他のモジュールのみが必要になります。

一部の型は、標準的な表現で単純であるかどうかにかかわらず、型が表現するはずの抽象概念で有効な値を表現しないプログラム内の値の構築を許可します。簡単な例としては、自己平衡二分探索木を表す型があります。コンストラクターにアクセスできるクライアント コードは、無効なツリーを簡単に構築できます。コンストラクターを公開するということは、外部から渡されたそのような値が無効である可能性があることを想定する必要があるため、奇妙な値に対しても適切な処理を行う必要があることを意味するか、インターフェースを操作するプログラマーが確実そうしないようにする責任があることを意味します。仮定に違反しないでください。通常は、そのような型がモジュールの外部で直接構築されないようにすることをお勧めします。

基本的に、それはあなたのタイプが表現するはずの概念に帰着します。コンパイラが必要な不変条件をチェックできないために、概念よりも「包括的」ではないデータ型の値に、非常に単純で明白な[2]方法で概念が直接マップされている場合、その概念はほとんど「データ型と同じ」であり、その構造を公開しても問題ありません。そうでない場合は、おそらく構造を非表示にしておく必要があります。


[1] ただし、座標値に使用している数値型を変更する可能性があるため、そのような変更の影響を最小限に抑える方法を考える必要があります。ただし、コンストラクターを公開するかどうかとはかなり直交しています。

[2] ここでの「自明」とは、10 人に独立して概念を表すデータ型を考え出すように依頼した場合、名前をモジュロ変更して、全員が同じことを返すことを意味します。

于 2012-09-07T01:40:07.787 に答える
1

私は、ほとんどの人とは異なる、著しく制限的なルールを提案します。中心的な基準は次のとおりです。

このタイプが決して変わらないことを保証しますか?もしそうなら、コンストラクターを公開するのは良い考えかもしれません。それでも頑張ってください!

しかし、その保証を行うことができるタイプは、、、またはのような非常に単純で一般的な「基礎」タイプである傾向がMaybeありEitherます[]

それらでさえ疑問視される可能性がありますが、それらは時々再訪されるためです。パフォーマンス上の理由から、さまざまなコンテキストでチャーチエンコードバージョンMaybeを使用したことがある人がいます。例:List

{-# LANGUAGE RankNTypes #-}

newtype Maybe' a = Maybe' { elimMaybe' :: forall r. r -> (a -> r) -> r }
nothing = Maybe' $ \z k -> z
just x = Maybe' $ \z k -> k x

newtype List' a = List' { elimList' :: forall r. (a -> r -> r) -> r -> r }
nil = List' $ \k z -> z
cons x xs = List' $ \k z -> k x (elimList' k z xs)

Maybe'これらの2つの例は、重要なことを強調しています。次の3つの関数をサポートしている限り、上記のタイプの実装を他の実装に置き換えることができます。

nothing :: Maybe' a
just :: a -> Maybe' a
elimMaybe' :: Maybe' a -> r -> (a -> r) -> r

...および次の法律:

elimMaybe' nothing z x  == z
elimMaybe' (just x) z f == f x

また、この手法は、あらゆる代数的データ型に適用できます。具体的なコンストラクターに対するパターンマッチングは、抽象的ではないと私は言います。抽象コンストラクタ+デストラクタパターンから抜け出せないものは実際には得られず、実装の柔軟性が失われます。

于 2012-09-07T17:44:38.697 に答える