Яка відповідь на функціональне програмування на основі типових інваріантів?


9

Мені відомо, що концепція інваріантів існує в декількох парадигмах програмування. Наприклад, циклічні інваріанти є актуальними в ОО, функціональному та процедурному програмуванні.

Однак один дуже корисний вид, знайдений в ООП, є інваріантом даних певного типу. Це те, що я називаю "типовими інваріантами" у назві. Наприклад, Fractionтип може мати numeratorа denominator, з інваріантом, що їх gcd завжди дорівнює 1 (тобто частка знаходиться у зменшеному вигляді). Я можу гарантувати це лише завдяки певному інкапсуляції типу, не дозволяючи його даних встановлювати вільно. Натомість мені ніколи не доводиться перевіряти, чи знижується вона, тому я можу спростити алгоритми, як перевірки рівності.

З іншого боку, якщо я просто декларую Fractionтип, не надаючи гарантії шляхом інкапсуляції, я не можу сміливо писати жодних функцій цього типу, які припускають, що частка зменшується, тому що в майбутньому хтось інший міг би підійти і додати шлях одержання частки, що не зменшується.

Як правило, відсутність такого роду інваріантів може призвести до:

  • Більш складні алгоритми, оскільки попередні умови потрібно перевірити / забезпечити у кількох місцях
  • Порушення сухих ситуацій, оскільки ці неодноразові попередні умови являють собою ті самі основні знання (що інваріант повинен бути правдою)
  • Необхідність виконувати попередні умови за допомогою відмов роботи, а не гарантій компіляції

Отже, моє запитання полягає в тому, що відповідь функціонального програмування на цей вид інваріанта. Чи існує функціонально-ідіоматичний спосіб досягти більш-менш того самого? Або є якийсь аспект функціонального програмування, який робить переваги менш актуальними?


багато функціональних мов можуть це робити тривіально ... Scala, F # та інші мови, які чудово грають з OOP, але Haskell теж ... в основному будь-яка мова, яка дозволяє визначати типи та їх поведінка, це підтримує.
AK_

@AK_ Я знаю, що F # може це зробити (хоча IIRC вимагає невеликих стрибків з обручем), і здогадався, що Скала може стати ще однією мовою крос-парадигми. Цікаво, що Haskell може це зробити - отримав посилання? Що я дійсно шукаю - це функціонально-ідіоматична відповідь, а не конкретні мови, які пропонують особливість. Але звичайно все може стати досить нечітким і суб'єктивним, коли ти починаєш говорити про те, що є ідіоматичним, і тому я не залишав це питання.
Бен Аронсон

У випадках, коли попередню умову неможливо перевірити під час компіляції, ідіоматично це перевірити в конструкторі. Розглянемо PrimeNumberклас. Виконати кілька зайвих перевірок первинності для кожної операції було б занадто дорого, але це не такий тест, який можна виконати під час компіляції. (Дуже багато операцій, які ви хотіли б виконати на простих числах, скажімо, множення, не утворюють закриття , тобто результати, ймовірно, не гарантовані простим. (Публікація як коментарі, оскільки я сам не знаю функціонального програмування.)
rwong

Начебто не пов’язане питання, але ... Чи важливіші твердження чи одиничні тести?
rwong

@rwong Так, приємні приклади є. Я насправді не на 100% зрозуміла, в якій кінцевій точці ви їдете.
Бен Аронсон

Відповіді:


2

Деякі функціональні мови, такі як OCaml, мають вбудовані механізми для реалізації абстрактних типів даних, завдяки чому застосовуються деякі інваріанти . Мови, які не мають таких механізмів, покладаються на те, щоб користувач "не дивився під килим", щоб застосувати інваріантів.

Абстрактні типи даних в OCaml

В OCaml модулі використовуються для структурування програми. Модуль має реалізацію та підпис , останній є своєрідним підсумком значень та типів, визначених у модулі, тоді як перший дає фактичні визначення. Це може бути порівняно з диптихом, .c/.hзнайомим програмістам C.

Як приклад, ми можемо реалізувати такий 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.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функцію.

Абстрактні типи даних у схемі

Мова схеми не має того самого механізму абстрактних типів даних, як це робить OCaml. Він покладається на те, що користувач "не дивиться під килим" для досягнення інкапсуляції.

У схемі прийнято визначати предикати, як 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 також використовують інкапсуляцію. Це просто у формі передачі декількох бажаних функцій для внесення певних видів модифікацій.


Ну, можливо, можна (і ідіоматично) писати код на C #, наприклад, що не дозволяє робити те, що ви там робили. І я думаю, є досить чітка різниця між тим, що один клас відповідає за примусовий виконання інваріанта, і кожною функцією, написаною будь-ким, де завгодно, де використовується певний тип, який повинен застосовувати той самий інваріант.
Бен Аронсон

@BenAaronson Зауважте різницю між "примусовим" та "розповсюдженням" інваріанта.
rwong

1
+1. Ця методика є ще більш потужною у FP, оскільки незмінні значення не змінюються; таким чином ви можете довести речі про них "раз і назавжди", використовуючи типи. Це не можливо для змінних об'єктів, тому що те, що для них зараз, може бути істинним пізніше; найкраще, що можна зробити, захисно переглянувши стан об’єкта.
Довал

@Doval Я цього не бачу. Якщо відмовитись від того, що більшість (?) Основних мов ОО мають спосіб зробити змінні незмінними. У OO у мене є: Створіть екземпляр, тоді моя функція мутує значення цього екземпляра таким чином, що може або не відповідає інваріанту. У FP у мене є: Створіть екземпляр, тоді моя функція створює другий екземпляр з різними значеннями таким чином, який може або не відповідає інваріанту. Я не бачу, як незмінність допомогла змусити мене відчувати себе впевненіше, що мій інваріант відповідає всім типам типу
Бен Аронсон

2
@BenAaronson Immutability не допоможе вам довести, що ви правильно реалізували свій тип (тобто всі операції зберігають певну інваріантність.) Я говорю, що це дозволяє поширювати факти про значення. Ви кодуєте певну умову (наприклад, це число є парним) у тип (перевіривши його в конструкторі), і отримане значення є доказом того, що початкове значення задовольнило умову. За допомогою об'єктів, що змінюються, ви перевіряєте поточний стан і зберігаєте результат в булі. Цей булевий корисний лише до тих пір, поки об'єкт не буде вимкнено, щоб умова була помилковою.
Довал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.