型ベースの不変条件に対する関数型プログラミングの答えは何ですか?


9

不変条件の概念が複数のプログラミングパラダイムに存在することを知っています。たとえば、ループ不変式は、オブジェクト指向プログラミング、関数型プログラミング、手続き型プログラミングに関連しています。

ただし、OOPにある非常に便利な種類の1つは、特定のタイプのデータの不変条件です。これをタイトルで「型ベースの不変条件」と呼んでいます。たとえば、Fractionタイプが持つかもしれないnumeratorし、denominatorそのGCDは常に1であることを不変で、(すなわち画分が減少した形です)。これを保証できるのは、そのタイプを何らかのカプセル化して、データを自由に設定できないようにすることだけです。その見返りに、削減されているかどうかを確認する必要がないので、等価性チェックなどのアルゴリズムを簡略化できます。

一方、Fractionカプセル化によってこの保証を提供せずに単純に型を宣言した場合、将来、他の誰かがやって来て方法を追加する可能性があるため、分数が減ると仮定してこの型の関数を安全に書くことはできません非還元分数を取得する方法。

一般に、この種の不変条件がないと、次のような結果になる可能性があります。

  • 前提条件を複数の場所でチェック/保証する必要があるため、より複雑なアルゴリズム
  • これらの繰り返される事前条件は同じ根本的な知識を表すため、DRY違反(不変条件が真であること)
  • コンパイル時の保証ではなく、実行時の失敗を通じて事前条件を適用する必要がある

だから私の質問は、この種の不変条件に対する関数型プログラミングの答えは何かということです。ほぼ同じことを達成するための機能的慣用的な方法はありますか?または、利点をあまり関連性のないものにする関数型プログラミングの側面はありますか?


多くの関数型言語はこれを簡単に行うことができます... Scala、F#、およびOOPでうまく機能する他の言語ですが、Haskellも...
AK_

@AK_私はF#がこれを行うことができることを知っています(IIRCにはいくつかの小さなフープジャンプが必要です)。また、Scalaは別のクロスパラダイム言語であると推測しました。Haskellがそれを行うことができるのは興味深いです-リンクを得ましたか?私が本当に探しているのは、機能を提供する特定の言語ではなく、機能的慣用的な答えです。しかし、もちろん、慣用的とは何かについて話し始めると、物事はかなり曖昧で主観的になる可能性があるため、私はそれを問題から外しました。
ベンアーロンソン2015年

コンパイル時に前提条件をチェックできない場合は、コンストラクターでチェックするのが慣用です。PrimeNumberクラスについて考えてみましょう。各操作の素数性について複数の冗長チェックを実行するのはコストがかかりすぎますが、コンパイル時に実行できる一種のテストではありません。(乗算などの素数に対して実行する多くの演算は、クロージャを形成しません。つまり、結果はおそらく素数が保証されていません。(私は関数型プログラミングを知らないため、コメントとして投稿します。)
rwong


@rwongええ、そこにいくつかの良い例があります。けれども、実際にあなたが運転している究極のポイントは100%明確ではありません。
Ben Aaronson、2015年

回答:


2

OCamlなどの一部の関数型言語には、抽象データ型を実装するメカニズムが組み込まれているため、一部の不変条件が適用されます。このようなメカニズムを持たない言語は、不変条件を強制するためにユーザーが「カーペットの下を見ない」ことに依存しています。

OCamlの抽象データ型

OCamlでは、モジュールはプログラムを構成するために使用されます。モジュールには実装シグネチャがあり、後者はモジュールで定義されている値と型の一種の要約ですが、前者は実際の定義を提供します。これは.c/.h、Cプログラマーがよく知っている2部作と大まかに比較できます。

例として、次のようにFractionモジュールを実装できます。

# module Fraction = struct
  type t = Fraction of int * int
  let rec gcd a b =
    match a mod b with
    | 0 -> b
    | r -> gcd b r

  let make a b =
   if b = 0 then
     invalid_arg "Fraction.make"
   else let d = gcd (abs a) (abs b) in
     Fraction(a/d, b/d)

  let to_string (Fraction(a,b)) =
    Printf.sprintf "Fraction(%d,%d)" a b

  let add (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*b2 + a2*b1) (b1*b2)

  let mult (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*a2) (b1*b2)
end;;

module Fraction :
  sig
    type t = Fraction of int * int
    val gcd : int -> int -> int
    val make : int -> int -> t
    val to_string : t -> string
    val add : t -> t -> t
    val mult : t -> t -> t
  end

この定義は次のように使用できます。

# Fraction.add (Fraction.make 8 6) (Fraction.make 14 21);;
- : Fraction.t = Fraction.Fraction (2, 1)

誰でも、組み込みのセーフティネットをバイパスして、fraction型の値を直接生成できますFraction.make

# Fraction.Fraction(0,0);;
- : Fraction.t = Fraction.Fraction (0, 0)

これを防ぐために、そのFraction.tような型の具体的な定義を隠すことができます:

# module AbstractFraction : sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end = Fraction;;

module AbstractFraction :
sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end

を作成する唯一の方法AbstractFraction.tは、AbstractFraction.make関数を使用することです。

Schemeの抽象データ型

Scheme言語には、OCamlと同じ抽象データ型のメカニズムはありません。カプセル化は、「カーペットの下を見ない」ユーザーに依存しています。

Schemeではfraction?、入力を検証する機会を与える値の認識などの述語を定義するのが慣例です。私の経験では、主な使用法は、ユーザーが値を偽造する場合、各ライブラリー呼び出しで入力を検証するのではなく、ユーザーが入力を検証できるようにすることです。

ただし、適用されたときに値を生成するクロージャーを返す、またはライブラリーによって管理されるプール内の値への参照を返すなど、戻り値の抽象化を強制するいくつかの戦略がありますが、実際にそれらを見たことはありません。


+1すべてのオブジェクト指向言語がカプセル化を強制しているわけではないことにも言及する価値があります。
マイケルショー

5

カプセル化は、OOPに付属する機能ではありません。適切なモジュール化をサポートする言語には、それがあります。

Haskellでの大まかな方法​​は次のとおりです。

-- Rational.hs
module Rational (
    -- This is the export list. Functions not in this list aren't visible to importers.
    Rational, -- Exports the data type, but not its constructor.
    ratio,
    numerator,
    denominator
    ) where

data Rational = Rational Int Int

-- This is the function we provide for users to create rationals
ratio :: Int -> Int -> Rational
ratio num den = let (num', den') = reduce num den
                 in Rational num' den'

-- These are the member accessors
numerator :: Rational -> Int
numerator (Rational num _) = num

denominator :: Rational -> Int
denominator (Rational _ den) = den

reduce :: Int -> Int -> (Int, Int)
reduce a b = let g = gcd a b
             in (a `div` g, b `div` g)

ここで、有理数を作成するには、不変式を強制する比率関数を使用します。データは不変であるため、後で不変条件に違反することはできません。

ただし、これにはコストがかかります。ユーザーが分母と分子の使用と同じ分解宣言を使用することはできなくなりました。


4

あなたはそれを同じように行います作るコンストラクタ制約を強制し、新しい価値を創造するたびにそのコンストラクタを使用することに同意します。

multiply lhs rhs = ReducedFraction (lhs.num * rhs.num) (lhs.denom * rhs.denom)

しかし、カール、OOP では、コンストラクタを使用することに同意する必要はありません。まあ、本当に?

class Fraction:
  ...
  Fraction multiply(Fraction lhs, Fraction rhs):
    Fraction result = lhs.clone()
    result.num *= rhs.num
    result.denom *= rhs.denom
    return result

実際、この種の虐待の機会はFPでは少ないです。不変性のため、コンストラクタを最後に配置する必要があります。私は、カプセル化を無能な同僚に対するある種の保護として、または通信の制約の必要性を取り除くものとして考えるのをやめてほしいと思います。それはしません。チェックする場所を制限するだけです。優れたFPプログラマーもカプセル化を使用します。これは、特定の種類の変更を行うために、いくつかの優先機能を通信するという形で提供されます。


たとえば、C#でコードを作成することは可能です(そして慣用的です)。そこでは、そこで行ったことを許可していません。そして、私は、不変条件を強制する責任を持つ単一のクラスと、同じ不変条件を強制しなければならない特定の型を使用する場所でだれかによって書かれたすべての関数との間にかなり明確な違いがあると思います。
ベンアーロンソン2015年

@BenAaronsonインバリアントの「強制」「伝播」の違いに注意してください。
rwong

1
+1。不変の値は変更されないため、この手法はFPでさらに強力です。したがって、型を使用して、「一度に」すべてについてそれらを証明できます。変更可能なオブジェクトではこれは不可能です。なぜなら、現在それらに当てはまることが後では当てはまらない可能性があるためです。防御的にオブジェクトの状態を再確認できる最善の方法です。
ドヴァル2015年

@ドヴァル私はそれを見ていません。さておき、ほとんどの(?)主要なオブジェクト指向言語には、変数を不変にする方法があります。オブジェクト指向では:インスタンスを作成し、その後、関数はそのインスタンスの値を不変条件に準拠する場合としない場合とで変更します。FPの場合:インスタンスを作成すると、関数は、不変条件に準拠する場合としない場合とで、異なる値を持つ2番目のインスタンスを作成します。私は不変性は私が私の不変のタイプのすべてのインスタンスのために適合していることを任意のより多くの自信を持ってメイクを支援してきましたどのように表示されていない
ベン・アーロンソン

2
@BenAaronsonの不変性は、型を正しく実装したことを証明するのに役立ちません(つまり、すべての操作で特定の不変条件が保持されます)。値についての事実を伝達できるということです。ある条件(この数値は偶数など)を型にエンコードし(コンストラクターでチェックすることにより)、生成される値は、元の値が条件を満たしたことの証明になります。変更可能なオブジェクトを使用して、現在の状態をチェックし、結果をブール値で保持します。そのブール値は、オブジェクトが変更されておらず、条件がfalseである限り有効です。
ドバル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.