що може піти не так у контексті функціонального програмування, якщо мій об’єкт є змінним?


9

Я бачу переваги змінних та незмінних об'єктів, таких як незмінні об'єкти, забирають багато важких для усунення проблем у багатопотоковому програмуванні завдяки спільному та записуваному стану. Навпаки, що змінюються об'єкти допомагають мати справу з ідентичністю об'єкта, а не створювати нову копію кожного разу, а також покращують продуктивність та використання пам'яті, особливо для великих об'єктів.

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

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

Я вважаю, що нижче моє власне твердження відповідає на це питання. Але все ж мені потрібен приклад, щоб я відчував це природніше.

OO допомагає керувати залежністю та писати простішу та ремонтоване програму за допомогою таких інструментів, як інкапсуляція, поліморфізм тощо.

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


1
@Ruben Я б сказав, що більшість функціональних мов дозволяють змінювати змінну, але не дозволяють використовувати їх, наприклад, змінні змінні мають інший тип
jk.

1
Я думаю, ти можеш змішати незмінне та незмінне у першому пункті?
jk.

1
@jk., він, звичайно, зробив. Відредаговано, щоб виправити це.
Девід Арно

6
@Ruben Функціональне програмування - парадигма. Як такий, він не вимагає функціональної мови програмування. І деякі мови fp, такі як F #, мають цю особливість .
Крістоф

1
@Ruben не конкретно я думав про Мварса в haskell hackage.haskell.org/package/base-4.9.1.0/docs/… різні мови мають різні рішення, звичайно, або IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html, хоча, звичайно, ви б використовували обидва зсередини монади
jk.

Відповіді:


7

Я думаю, що важливість найкраще демонструється порівнянням з підходом до ОО

наприклад, скажімо, у нас є об’єкт

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

У парадигмі OO метод додається до даних, і є сенс, щоб ці дані мутувались методом.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

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

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

Чи очікуєте ви замовлення.Status == "Придбано"?

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

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Якщо замовлення було змінено функцією покупки, купуєтьсяOrder2 не вдалося.

Визначаючи речі як результати функцій, це дозволяє нам використовувати ці результати, не фактично їх обчислюючи. Що в плані програмування - це відкладене виконання.

Це може бути корисно саме по собі, але, коли ми не впевнені, коли функція насправді відбудеться, і ми з цим все добре, ми можемо використовувати паралельну обробку набагато більше, ніж ми можемо в парадигмі ОО.

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

Якщо функція мутує свій внесок, ми мусимо бути набагато уважнішими щодо таких речей.


Дякую !! дуже корисний. Тож нова реалізація закупівлі виглядатиме Order Purchase() { return new Order(Status = "Purchased") } так, що статус читається лише у полі. ? Знову ж таки, чому ця практика є більш актуальною в контексті парадигми програмування функцій? Переваги, які ви згадали, можна побачити і в програмуванні ОО, правда?
rahulaga_dev

в OO ви очікуєте, щоб object.Purchase () змінив об'єкт. Ви можете зробити це непорушним, але тоді, чому б не перейти до повноцінної функціональної парадигми
Еван

Я думаю, що проблему мені доведеться візуалізувати, оскільки я чистий розробник c #, який є об'єктом орієнтований на природу. Тож те, що ви говорите мовою, яка охоплює функціональне програмування, не вимагатиме функції "Purchase ()", що повертає придбане замовлення, щоб бути приєднаним до будь-якого класу чи об'єкта, правда?
rahulaga_dev

3
ви можете написати функціональний c # змінити об'єкт на структуру, зробити його непорушним і написати функцію <Замовлення, замовлення> Купівля
Ewan

12

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

Ключове, що слід запитати, в чому полягає користь непорушності? Відповідь - це уникає складності. Скажімо, у нас є дві змінні, xі y. Обидва починаються зі значення 1. yхоча подвоюється кожні 13 секунд. Яка буде цінність кожного з них через 20 днів? xбуде 1. Це легко. Хоча знадобиться все-таки намагатися, yяк це зробити складніше. Який час доби за 20 днів? Чи потрібно враховувати літній час? Складність yпорівняно з xцим набагато більша.

І це відбувається і в реальному коді. Кожен раз, коли ви додаєте змішане значення до суміші, це стає ще одним складним значенням для утримання та обчислення в голові чи на папері при спробі написання, читання чи налагодження коду. Чим складніше, тим більше шанси на те, що ти помилишся і введеш помилку. Код важко записати; важко читати; важко налагодити: код важко отримати правильно.

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


4
"Один з найпростіших способів зменшити складність - зробити речі непорушними за замовчуванням і зробити їх незмінними лише при необхідності": Дуже приємне та стисле резюме.
Джорджіо

2
@DavidArno Складність, яку ви описуєте, дуже важко міркувати. Ви також торкнулися цього, сказавши: "Код важко написати; важко читати; важко налагоджувати; ...". Мені подобаються незмінні об'єкти, тому що вони набагато простіше міркувати про себе, не тільки я, але і спостерігачі, які дивляться далі, не знаючи всього проекту.
розібрати номер-5

1
@RahulAgarwal, " Але чому ця проблема стає більш помітною в контексті функціонального програмування ". Це не так. Я думаю, що, можливо, мене бентежить те, про що ви ставите запитання, оскільки проблема є набагато менш вираженою на ПП, оскільки FP заохочує незмінність, таким чином уникаючи проблеми.
Девід Арно

1
@djechlin, " Як ваш 13-й приклад стане простішим для аналізу за допомогою непорушного коду? " Він не може: yповинен мутувати; це вимога. Іноді нам потрібно мати складний код, щоб відповідати складним вимогам. Я намагався зробити те, що слід уникати зайвих складностей. Значення, що мутують, за своєю суттю складніші за фіксовані, тому - щоб уникнути зайвої складності - мутуйте значення лише тоді, коли вам доведеться.
Девід Арно

3
Змінюваність створює кризу ідентичності. Ваша змінна більше не має єдиної ідентичності. Натомість її особистість зараз залежить від часу. Тож символічно, замість одного x у нас тепер є сім'я x_t. Будь-який код, що використовує цю змінну, тепер також повинен турбуватися про час, викликаючи додаткову складність, зазначену у відповіді.
Алекс Вонг

8

що може піти не так у контексті функціонального програмування

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

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

В основному, якщо це погано, це погано незалежно від ОО або функціональної парадигми програмування, правда?

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


4

Щойно я відповів на питання StackOverflow, яке досить добре ілюструє ваше запитання. Основна проблема змінних структур даних полягає в тому, що їхня ідентичність є дійсною лише в один момент часу, тому люди, як правило, стискаються настільки, наскільки вони можуть в маленьку точку коду, де вони знають, що ідентичність є постійною. У цьому конкретному прикладі робиться багато журналу всередині циклу for:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

Коли ви звикли до незміненості, немає побоювання зміни структури даних, якщо ви чекатимете занадто довго, тож ви можете робити завдання, які логічно розділені у ваш дозвілля, набагато більш відокремленими:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}

3

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

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

Я думаю, що для програмування взагалі (не лише функціонального програмування) найкорисніше вважати незмінні об'єкти як такі, що потрапляють у три категорії:

  1. Об'єкти, які не можуть дозволити чомусь змінити їх, навіть із посиланням. Такі об'єкти та посилання на них поводяться як цінності і можуть вільно ділитися ними.

  2. Об'єкти, які дозволяли б їх змінити за кодом, на який є посилання на них, але посилання на які ніколи не будуть піддаватися жодному коду, який би насправді їх змінив. Ці об'єкти інкапсулюють значення, але їм можна ділитися лише з кодом, якому можна довіряти, щоб не змінювати їх або піддавати їх коду, який може зробити.

  3. Об'єкти, які будуть змінені. Ці об'єкти найкраще розглядати як контейнери та посилатися на них як на ідентифікатори .

Корисною схемою часто є те, щоб об’єкт створив контейнер, заповнив його за допомогою коду, якому можна довіряти, щоб не зберігати посилання після цього, і тоді єдині посилання, які коли-небудь існуватимуть у Всесвіті, повинні бути в коді, який ніколи не змінюватиме об'єкт, коли він заселений. Хоча контейнер може мати тип мутації, він може міркувати про (*) як би незмінний, оскільки нічого насправді його ніколи не буде мутувати. Якщо всі посилання на контейнер зберігаються в непорушних типах обгортки, які ніколи не змінять його вміст, такі обгортки можуть бути передані навколо безпечно, як ніби дані всередині них зберігалися в незмінних об'єктах, оскільки посилання на обгортки можуть вільно ділитися та перевірятися на в будь-який час.

(*) У багатопотоковому коді може знадобитися використання "бар'єрів пам'яті" для того, щоб перед тим, як будь-яка нитка змогла побачити будь-яку посилання на обгортку, наслідки всіх дій на контейнері були б видимі для цього потоку, але це окремий випадок, згаданий тут лише для повноти.


дякую за вражаючу відповідь !! Я думаю, що, ймовірно, джерело моєї плутанини - це те, що я перебуваю з c # background і навчаюсь "написання коду функціонального стилю в c #", який продовжує говорити про те, щоб уникати змінних об'єктів - але я думаю, що мови, які охоплюють парадигму функціонального програмування, просувають (або застосовують - не впевнений якщо примусово правильно використовувати) незмінність.
rahulaga_dev

@RahulAgarwal: Можна посилатися на об'єкт, який інкапсулює значення , на значення якого не впливає існування інших посилань на той самий об'єкт, має ідентифікацію, яка асоціювала б їх з іншими посиланнями на той самий об'єкт, або ні на одну. Якщо стан реальних слів зміниться, то або значення, або ідентичність об'єкта, пов'язаного з цим станом, можуть бути постійними, але не обидва - потрібно буде змінити. 50 000 доларів - це те, що робити.
supercat

1

Як уже згадувалося, проблема зі змінним станом є в основному підкласом більшої проблеми побічних ефектів , де тип повернення функції точно не описує, що функція насправді робить, тому що в цьому випадку вона також робить мутацію стану. Цю проблему вирішили деякі нові дослідницькі мови, такі як F * ( http://www.fstar-lang.org/tutorial/ ). Ця мова створює систему ефектів, подібну до системи типів, де функція не тільки статично оголошує свій тип, але і його ефекти. Таким чином, абоненти функції усвідомлюють, що при виклику функції може виникати мутація стану, і цей ефект поширюється на її викликаючих.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.