Значення "запам'ятовування" у функціональному програмуванні


20

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

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

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

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

(Далі наведено всі псевдокоди)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

У функціональному програмуванні я не був впевнений. Ось що я придумав:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

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


32
Якщо ви хочете сприймати ваш приклад серйозно, нехай він вирішить практичну задачу, а не математичну задачу. Це свого роду кліше серед розробників, що "всі FP хороші для вирішення математичних задач", і якщо ваш приклад - Ще одна математична функція, ви тільки підсилюєте стереотип, а не робите те, що ви робите, виглядати корисним.
Мейсон Уілер

12
Ваша спроба насправді досить гарна, якщо врахувати реальні міркування. Усі ваші рекурсивні дзвінки - це хвостові дзвінки , тобто після виклику функція не робить нічого іншого. Це означає, що компілятор або інтерпретатор, який його підтримує, може оптимізувати їх, щоб ваша рекурсивна функція використовувала фіксовану кількість пам'яті стека, а не суму, пропорційну y.
8bittree

1
Дякую за підтримку! Я все ще дуже новачок у цьому, тому мій псевдокод не ідеальний. @MasonWheeler Я думаю, що в цьому випадку мій код насправді не сприймається серйозно. Я все ще вчуся, і причина, що я люблю FP, це тому, що це Math-y. Весь сенс мого прикладу - пояснити моєму вчителю, чому я використовую FP. Він насправді не розуміє, що це таке, тому це здавалося гарним способом показати йому переваги.
Ученна

5
Якою мовою ви плануєте написати код? Не намагайтеся використовувати стиль, який не підходить для мови, якою ви користуєтесь.
Carsten S

Відповіді:


37

Перш за все, привітання з "побаченням світла". Ви зробили світ програмного забезпечення кращим місцем, розширивши свій кругозір.

По-друге, немає сумніву, що професор, який не розуміє функціонального програмування, не зможе сказати нічого корисного щодо вашого коду, крім охайних коментарів, таких як "відступ виглядає". Це не дивно в курсі веб-розробки, оскільки більшість веб-розробок здійснюється за допомогою HTML / CSS / JavaScript. Залежно від того, наскільки ви насправді піклуєтесь про вивчення веб-розробок, можливо, ви захочете докласти зусиль, щоб засвоїти інструменти, які викладає ваш професор (боляче, хоча це може бути - я знаю з досвіду).

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

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

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

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

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

а не зміна значення змінної x.


Ваш рекурсивний powне зовсім правильний. Як є, pow 3 3повертається 81, а не 27. Це повинно бутиelse x * pow x (y-1).
8bittree

3
Ну, написання правильного коду важко :) Виправлено, і я також додав анотації типу. @Ucenna Це повинен бути SML, але я його не використовував деякий час, тому я міг би синтаксис трохи помилитися. Занадто багато проклятих способів оголосити функцію, я ніколи не можу згадати правильне ключове слово. Крім змін синтаксису, код є ідентичним у JavaScript.
садок

2
@jwg Javascript має деякі функціональні аспекти: функції можуть визначати вкладені функції, повертати функції та приймати функції як параметри; він підтримує закриття з лексичним розмахом (хоча жодного диспетчерського діапазону, однак, немає). Це залежить від дисципліни програміста, щоб утримуватися від зміни стану та мутування даних.
Каспер ван ден Берг

1
@jwg Не існує узгодженого визначення поняття "функціональна" мова (ні для "імперативної", "об'єктно-орієнтованої" чи "декларативної"). Я намагаюся утримуватися від використання цих термінів, коли це можливо. Занадто багато мов під сонцем, щоб їх можна класифікувати на чотири акуратні групи.
садок

1
Популярність - це жахлива метрика, тому коли колись хтось згадує про те, що мова чи інструмент X повинен бути кращим, тому що це так широко використовується, я знаю, що продовжувати аргументи було б безглуздо. Я більше знайомий із сімейством мов ML, ніж особисто Haskell. Але я також не впевнений, чи це правда; гадаю, переважна більшість розробників не спробували в першу чергу Haskell.
садок

33

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

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

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

Бере список значень xsі початковий стан з 0(представленого totalв даному випадку). Потім, для кожного xв xs, ми поєднуємо це значення з поточним станом відповідно до деякої комбінуючої операції (в даному випадку доповнення) і використовуємо результат як новий стан. По суті, sum_all([1, 2, 3])еквівалентний (3 + (2 + (1 + 0))). Цей шаблон може бути вилучений у функцію вищого порядку, функцію , яка приймає функції як аргументи:

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

Ця реалізація foldзалишається вкрай необхідною, але це може бути здійснено і рекурсивно:

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

Виражена у вигляді складки, ваша функція проста:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

... де repeat(x, n)повертається список nкопій x.

Багато мов, особливо ті, які спрямовані на функціональне програмування, пропонують скласти у своїй стандартній бібліотеці. Навіть Javascript надає це під назвою reduce. Загалом, якщо ви виявите, що використовуєте рекурсію, щоб "запам'ятати" значення через певний цикл, ви, мабуть, хочете скласти.


8
Однозначно навчіться визначати, коли проблему можна вирішити складкою або картою. У FP майже всі петлі можуть бути виражені у вигляді складок або карти; тому явна рекурсія зазвичай не потрібна.
Carcigenicate

1
На деяких мовах ви можете просто написатиfold(repeat(base, power), 1, *)
user253751

4
Ріко Калер: scanпо суті, це те, foldде замість того, щоб просто поєднувати список значень в одне значення, воно поєднується, і кожне проміжне значення викидається назад по шляху, створюючи список усіх проміжних станів, складених складовою, а не просто створюючи остаточний стан. Це реально з точки зору fold(кожна циклічна операція).
Джек

4
@RicoKahler І, наскільки я можу сказати, скорочення і складки - це одне і те ж. Haskell використовує термін "скласти", тоді як Clojure вважає за краще "зменшити". Їх поведінка здається мені однаковою.
Carcigenicate

2
@Ucenna: Це і змінна, і функція. У функціональному програмуванні функції є значеннями так само, як числа та рядки - ви можете зберігати їх у змінних, передавати їх як аргументи іншим функціям, повертати їх із функцій та взагалі трактувати їх як інші значення. Так combiner_funcє аргументом і sum_allпередає анонімну функцію (це lambdaбіт - він створює значення функції, не називаючи її), який визначає, як він хоче об'єднати два елементи разом.
Джек

8

Це додаткова відповідь, яка допоможе пояснити карти та складки. Для наведених нижче прикладів я буду використовувати цей список. Пам'ятайте, що цей список незмінний, тому він ніколи не зміниться:

var numbers = [1, 2, 3, 4, 5]

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

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

Найпростіший приклад цього - просто додати число до кожного номера у списку. Я буду використовувати псевдокод, щоб зробити його агностичним:

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

Якби ви надрукували numbers2, ви побачили б, [3, 4, 5, 6, 7]який перший список із 2-ма доданими до кожного елемента. Зверніть увагу, функція add-twoбула надана mapдля використання.

Папки складаються аналогічно, за винятком функції, яку вам потрібно надати, потрібно взяти 2 аргументи. Перший аргумент - це зазвичай акумулятор (у лівій складці, які є найпоширенішими). Акумулятор - це дані, передані під час циклу. Другий аргумент - це поточний пункт списку; як і вище для mapфункції.

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

Якщо ви надрукували, sumви побачили б суму списку чисел: 15.

Ось які аргументи потрібно foldзробити:

  1. Це функція, яку ми надаємо складці. Згинання передасть функцію акумулятора струму та поточного елемента списку. Незалежно від функції, яка повертається, стане новим акумулятором, який буде переданий функції наступного разу. Ось так ви «запам’ятовуєте» значення, коли ви циклічно використовуєте FP-стиль. Я дав йому функцію, яка бере 2 числа і додає їх.

  2. Це початковий акумулятор; що запускає акумулятор, як і раніше, ніж будь-які елементи у списку обробляються. Коли ви підсумовуєте числа, яка загальна сума, перш ніж ви додали будь-які цифри разом? 0, який я передав як другий аргумент.

  3. Нарешті, як і з картою, ми також передаємо список номерів для її обробки.

Якщо складки все ще не мають сенсу, врахуйте це. Коли ви пишете:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

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

[1, 2, 3, 4, 5]

Стає:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

Що дорівнює 15.

Використовуйте, mapколи ви хочете перетворити один список в інший, однакової довжини.

Використовуйте, foldколи ви хочете перетворити список в єдине значення, наприклад підсумовуючи список чисел.

Як зазначав @Jorg в коментарях, "єдине значення" не повинно бути чимось простим, як число; це може бути будь-який єдиний об'єкт, включаючи список або кортеж! Те, як я насправді мав складки, для мене полягав у визначенні карти з точки зору складки. Зверніть увагу, як складається аккумулятор у списку:

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

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


1
@Ucenna @Ucenna Є кілька вад у вашому коді (як iніколи не визначено), але я думаю, у вас є правильна ідея. Одна з проблем вашого прикладу: функція ( x) передається за один раз лише одним елементом списку, а не всім списком. Коли перший xвиклик викликається, він передається вашому початковому акумулятору ( y) як першому аргументу та першому елементу як другому аргументу. Наступного разу, коли він буде запущений, xбуде передано новий акумулятор зліва (що б не xповернувся перший раз) та другий елемент списку як другий аргумент.
Carcigenicate

1
@Ucenna Тепер, коли у вас є основна ідея, погляньте на реалізацію Джека ще раз.
Carcigenicate

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

3
"Використовуйте, foldколи ви хочете перетворити список в єдине значення (наприклад, підсумовуючи список чисел)." - Я просто хочу зазначити, що це "єдине значення" може бути довільно складним ... включаючи список! Власне, foldце загальний метод ітерації, він може зробити все , що може зробити ітерація. Напр., mapМожна тривіально виразити як func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)Here, "єдине значення", яке обчислюється, foldє насправді списком.
Йорг W Міттаг

2
... можливо, хочете зробити зі списком, можна зробити fold. І це не обов'язково повинен бути списком, і кожна колекція, яка може бути виражена як порожня / не порожня, буде робити. Що в основному означає, що це зробить будь-який ітератор. (я думаю, що кидати слово "катаморфізм" було б занадто багато для початківця, хоча :-D)
Йорг W Міттаг

1

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

Наприклад, у haskell у вас вже є функція (^)у Prelude.

Або якщо ви хочете зробити це більш програмно product (replicate y x)

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


1
Щоб логічно пов’язати цю відповідь з іншими, слід зазначити, що productце лише функція швидкого доступу до foldмноження як її функції та 1 як її початковий аргумент, і replicateце функція, яка виробляє ітератор (або список; як я зазначив вище два по суті не відрізняються від haskell), що дає задану кількість однакових виходів. Тепер слід легко зрозуміти, як ця реалізація робить те саме, що і відповідь @ Jack вище, просто використовуючи попередньо визначені спеціальні версії тих же функцій, щоб зробити її більш простою.
Periata Breatta
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.