Програмуючи у функціональному стилі, чи є у вас єдиний стан програми, який ви плетете за допомогою логіки програми?


12

Як побудувати систему, яка має все перелічене нижче :

  1. Використання чистих функцій з незмінними об'єктами.
  2. Передавайте лише дані про функції, які потрібні цій функції, не більше (тобто немає великого об'єкта стану програми)
  3. Уникайте занадто багато аргументів до функцій.
  4. Уникайте необхідності конструювання нових об'єктів лише для упаковки та розпакування параметрів до функцій, просто щоб уникнути занадто великої кількості параметрів, переданих функціям. Якщо я збираюсь пакувати кілька елементів у функції як один об'єкт, я хочу, щоб цей об'єкт був власником цих даних, а не тим, що створюється тимчасово

Мені здається, що державна монада порушує правило №2, хоча це не очевидно, оскільки воно сплетене через монаду.

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

Фон

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

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

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

Виходячи з цього, я переглянув свою програму і: 1. По-перше, увесь стан мого додатка поставте в один глобальний об'єкт (GameState) 2. По-друге, я зробив GameState незмінним. Ви не можете його змінити. Якщо вам потрібна зміна, вам доведеться побудувати нову. Я зробив це, додавши конструктор копій, який необов'язково займає одне або кілька полів, які змінилися. 3. Кожній програмі я передаю в GameState як параметр. У межах функції, після того, як робить те, що збирається, він створює новий GameState і повертає його.

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

Моє запитання:

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

Щось із цього виглядає неправильним. Функція не потребує усього GameState. Просто потрібний об'єкт Score. Тому я оновив його, щоб пройти Оцінку, і повернути лише Оцінку.

Це, здавалося, має сенс, тому я пішов далі з іншими функціями. Деякі функції вимагають, щоб я передавав 2, 3 або 4 параметри від GameState, але, оскільки я використовував шаблон у всьому зовнішньому ядрі програми, я передаю все більше і більше стану програми. Мовляв, у верхній частині циклу робочого процесу я б назвав метод, який би викликав метод, який би викликав метод тощо, аж до місця, де обчислюється оцінка. Це означає, що поточний бал передається через усі ці шари лише тому, що функція в самому низу збирається обчислити бал.

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

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

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


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

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

Для мене ваше питання здавалося ширшим, що лише констатує. Якщо це тільки про управління державами ось початок: побачити відповідь і посилання в stackoverflow.com/questions/1020653 / ...
Walfrat

2
@DaishaLynn Я не думаю, що слід видаляти питання. Це було схвалено, і ніхто не намагається його закрити, тому я не думаю, що це не вдається для цього веб-сайту. Відсутність відповіді поки що може бути лише тому, що вона вимагає певних відносно нішевих знань. Але це не означає, що він не буде знайдений і відповіде врешті-решт.
Бен Аронсон

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

Відповіді:


2

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

  • Розподіліть GameStateієрархічно, так що ви отримаєте 3-5 менших деталей замість 15.
  • Нехай він реалізує інтерфейси, тому ваші методи бачать лише необхідні частини. Ніколи не кидайте їх назад, як би ви брехали собі про реальний тип.
  • Нехай також деталі реалізують інтерфейси, щоб ви мали тонкий контроль над тим, що передаєте.
  • Використовуйте об'єкти параметрів, але робіть це економно і намагайтеся перетворити їх на реальні об'єкти своєю поведінкою.
  • Іноді проходження трохи більше, ніж потрібно, краще, ніж довгий список параметрів.

Тож зараз мені цікаво, чи є у мене проблема в тому, що мої функції вкладені занадто глибоко.

Я не думаю, що так. Рефакторинг на невеликі функції - це правильно, але, можливо, ви могли б перегрупувати їх краще. Іноді це неможливо, іноді просто потрібен другий (або третій) погляд на проблему.

Порівняйте свій дизайн із змінним. Чи є речі, які пережили перепис? Якщо так, то чи не зможете ви покращити їх так само, як ви робили раніше?


Хтось сказав мені змінити дизайн, щоб коли-небудь функція приймала лише один параметр, щоб я міг використовувати каррі. Я спробував цю одну функцію, тож замість виклику DeleteEntity (a, b, c) тепер я викликаю DeleteEntity (a) (b) (c). Так це мило, і це повинно зробити його більш компонованими, але я просто не отримую цього ще.
Daisha Lynn

@DaishaLynn Я використовую Java, і немає солодкого синтаксичного цукру для каррі, тому (для мене) це не варто намагатися. Я досить скептично ставлюсь до можливого використання функцій вищого порядку в нашому випадку, але дайте мені знати, чи спрацювало воно для вас.
maaartinus

2

Я не можу розмовляти з C #, але в Haskell ви б перейшли до всього стану. Це можна зробити явно або з монадою держави. Одне, що ви можете зробити, щоб вирішити питання функцій, які отримують більше інформації, ніж їм потрібно, - це використовувати класи типу type. (Якщо ви не знайомі, типи класів Haskell трохи схожі на інтерфейси C #.) Для кожного елемента Е стану, ви можете визначити клас типу HasE, який вимагає функції getE, яка повертає значення E. Після цього монада стану зробив екземпляр усіх цих класів. Тоді у своїх фактичних функціях замість того, щоб явно вимагати монади штату, вам потрібна будь-яка монада, що належить до класів типу Has для потрібних вам елементів; що обмежує те, що функція може виконувати з монадою, яку вона використовує. Детальніше про цей підхід див. У статті Майкла Снайманадопис за схемою дизайну ReaderT .

Можливо, ви могли б повторити щось подібне в C #, залежно від того, як ви визначаєте стан, який передається навколо. Якщо у вас є щось подібне

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

ви могли б визначати інтерфейси IHasMyIntі IHasMyStringз методами, GetMyIntі GetMyStringвідповідно. Потім клас класу виглядає так:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

то ваші методи можуть вимагати IHasMyInt, IHasMyString або всієї MyState, якщо це доречно.

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

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}

Це цікаво. Отже, зараз, коли я передаю функцію виклику і передаю 10 параметрів за значенням, я проходжу в "gameSt'ate" 10 разів, але 10 різних типів параметрів, таких як "IHasGameScore", "IHasGameBoard" тощо. було способом передачі пропустити один параметр, який може вказати функція, щоб реалізувати всі інтерфейси цього типу. Цікаво, чи можу я це зробити з "загальним обмеженням" .. Дозвольте спробувати це.
Даїша Лінн

1
Це спрацювало. Ось він працює: dotnetfiddle.net/cfmDbs .
Daisha Lynn

1

Я думаю, вам було б добре дізнатися про Redux або Elm і як вони вирішують це питання.

В основному, у вас є одна чиста функція, яка приймає весь стан і дії, які виконував користувач, і повертає новий стан.

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

Щоб дізнатися більше, Google Elm Architecture або Redux.js.org.


Я не знаю Елма, але я вважаю, що він схожий на Редукс. У Redux, чи не викликають усі редуктори для кожної зміни штату? Звучить надзвичайно неефективно.
Дайша Лінн

Що стосується оптимізації низького рівня, не варто вважати, що вимірювати. На практиці це досить швидко.
Даніель Т.

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

-2

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

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

C # ще не готовий використовуватись настільки функціонально, як ви хотіли б.


3
Не в захваті від цієї відповіді, ні тоном. Я нічого не зловживаю. Я розсуваю межі C #, щоб використовувати його як більш функціональну мову. Це не рідкість робити. Ви, здається, по-філософськи проти цього, що добре, але в цьому випадку не дивіться на це питання. Ваш коментар нікому не корисний. Рухайся.
Даїша Лінн

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

Я цього не роблю, тому що це хіп. Сам C # рухається до використання функціонального стилю. Сам Андерс Хейльсберг вказав як такого. Я розумію, що вас цікавить лише використання мови в основному потоці, і я розумію, чому і коли це доречно. Я просто не знаю, чому хтось, як ти, навіть на цій темі. Як ти реально допомагаєш?
Даїша Лінн

@DaishaLynn, якщо ви не можете впоратися з відповідями, що критикують ваше питання чи підхід, ви, мабуть, не повинні задавати тут запитання, або наступного разу вам слід просто додати відмову, сказавши, що вас цікавлять лише відповіді, що підтримують вашу ідею на 100%, тому що ви не хочете почути правду, а швидше отримати підтримку.
t3chb0t

Будьте трохи сердечнішими один до одного. Можна давати критику без зневаги до мови. Спроба програмування C # у функціональному стилі, звичайно, не є "зловживанням" або кращим випадком. Це звичайна методика, яка використовується багатьма розробниками C # для навчання інших мов.
zumalifeguard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.