함수형 프로그래밍에서 "기억"값


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
실제 고려 사항을 고려할 때 실제로 시도는 꽤 좋습니다. 모든 재귀 호출은 tail 호출입니다 . 즉, 함수는 호출 후 다른 작업을 수행하지 않습니다. 즉, 이를 지원 하는 컴파일러 또는 인터프리터 는이 를 최적화하여 재귀 함수가에 비례하는 양이 아닌 고정 된 양의 스택 메모리를 사용하도록 할 수 있습니다 y.
8bittree

1
지원해 주셔서 감사합니다! 나는 여전히 이것에 매우 익숙하므로 의사 코드는 완벽하지 않습니다. @MasonWheeler이 경우 내 코드는 실제로 심각하게 다루어지지 않습니다. 나는 아직도 배우고 있는데, FP를 좋아하는 이유는 그것이 Math-y이기 때문입니다. 내 모범의 요점은 선생님에게 왜 FP를 사용하는지 설명하는 것입니다. 그는 그것이 무엇인지 실제로 이해하지 못하므로 이점을 보여줄 수있는 좋은 방법처럼 보였습니다.
Ucenna

5
어떤 언어로 코드를 작성 하시겠습니까? 사용중인 언어에 적합하지 않은 스타일을 사용하지 마십시오.
Carsten S

2
아마도 유용 할 것 : en.wikipedia.org/wiki/Memoization
Ethan Bolker

답변:


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에서 동일합니다.
gardenhead

2
@jwg Javascript에는 몇 가지 기능적인 측면이 있습니다. 함수는 중첩 함수를 정의하고 함수를 반환하며 함수를 매개 변수로 사용할 수 있습니다. 어휘 범위를 가진 클로저를 지원합니다 (그렇지만 lisp 동적 범위는 없습니다). 상태 변경 및 데이터 변경을 자제하는 것은 프로그래머의 징계에 달려 있습니다.
카스퍼 반 덴 버그

1
@jwg "기능적"언어 ( "제 국적", "객체 지향적"또는 "선언적"의 정의)에 대한 동의는 없다. 가능할 때마다이 용어를 사용하지 마십시오. 태양 아래에 네 개의 깔끔한 그룹으로 분류하기에는 너무 많은 언어가 있습니다.
gardenhead

1
인기는 끔찍한 척도이므로 누군가가 언어 또는 도구 X가 널리 사용되기 때문에 더 좋아야한다고 언급 할 때마다 논쟁을 계속하는 것이 의미가 없다는 것을 알고 있습니다. 저는 개인적으로 Haskell보다 ML 언어에 익숙합니다. 그러나 그것이 사실인지 잘 모르겠습니다. 내 생각에 대다수의 개발자가 처음부터 Haskell을 시도 하지 않았을 것입니다.
gardenhead

33

이것은 실제로 가든 헤드의 답변에 대한 부록이지만,보고있는 패턴의 이름이 접 히고 있음을 지적하고 싶습니다.

함수형 프로그래밍에서 접기 는 각 작업 사이의 값을 "기억하는"일련의 값을 결합하는 방법입니다. 숫자 목록을 반드시 추가하십시오.

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

우리는 값 목록 수행 xs하고 , 초기 상태0(의해 표현 total이 경우에는). 그런 다음 각 xin 에 대해 일부 결합 작업 (이 경우 추가) 에 따라 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))

... where repeat(x, n)n사본 목록을 리턴합니다 x.

많은 기능, 특히 함수형 프로그래밍에 적합한 언어는 표준 라이브러리에서 접기를 제공합니다. Javascript조차도 이름으로 제공합니다 reduce. 일반적으로 재귀를 사용하여 어떤 종류의 루프에서 값을 "기억"하는 경우 접을 수 있습니다.


8
폴드 나 맵으로 문제를 해결할 수있는 시점을 확실히 파악하십시오. FP에서 거의 모든 루프는 접기 또는 맵으로 표현 될 수 있습니다. 따라서 명시적인 재귀는 필요하지 않습니다.
Carcigenicate

1
일부 언어에서는 다음과 같이 작성할 수 있습니다.fold(repeat(base, power), 1, *)
user253751

4
Rico Kahler : scan기본적 fold으로 값 목록을 하나의 값으로 결합하는 대신 결합되어 각 중간 값이 다시 튀어 나와서 중간이 아닌 모든 중간 상태 목록이 생성됩니다. 최종 상태. fold(모든 루핑 작업은) 측면에서 구현할 수 있습니다.
Jack

4
@RicoKahler 그리고 제가 알 수있는 한 축소와 접기는 같은 것입니다. Haskell은 "fold"라는 용어를 사용하지만 Clojure는 "reduce"를 선호합니다. 그들의 행동은 나에게 동일하게 보인다.
Carcigenicate

2
@Ucenna : 그것은는 모두 변수 및 기능. 함수형 프로그래밍에서 함수는 숫자 및 문자열과 같은 값입니다. 변수에 변수를 저장하고 다른 함수에 인수로 전달하고 함수에서 반환하며 일반적으로 다른 값처럼 처리 할 수 ​​있습니다. 그래서 combiner_func인수하고, sum_all통과 익명 함수를 (즉,의 lambda비트 - 그것을 이름을 지정하지 않고 함수 값을 생성)은 두 개의 항목을 결합하고자하는 방법을 정의합니다.
Jack

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.

접기 기능은 두 가지 인수를 가져와야 한다는 점을 제외하면 비슷합니다. 첫 번째 인수는 일반적으로 누산기 (왼쪽 접힘)가 가장 일반적입니다. 누산기는 루핑하는 동안 전달되는 데이터입니다. 두 번째 인수는 목록의 현재 항목입니다. 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 이제 기본 아이디어를 얻었으므로 Jack의 구현을 다시 살펴보십시오.
Carcigenicate

1
@Ucenna : 불행하게도, 폴드마다 주어진 함수가 누산기를 첫 번째 또는 두 번째 인수로 사용할지 여부에 따라 각기 다른 언어가 서로 다른 기본 설정을 갖습니다. 접기를 가르치기 위해 덧셈과 같은 정류 연산을 사용하는 것이 좋은 이유 중 하나입니다.
Jack

3
" fold목록을 단일 값으로 바꾸려면 (숫자 목록을 합산하는 것과 같이) a를 사용하십시오." –이 "단일 가치"는 목록을 포함하여 임의로 복잡 할 수 있습니다. 실제로 fold일반적인 반복 방법이며 반복이 할 수있는 모든 작업을 수행 할 수 있습니다. 예 map를 들어 func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)여기에서 계산 된 "단일 값" fold은 실제로 목록입니다.
Jörg W Mittag

2
… 아마도 목록으로하고 싶을 수도 있습니다 fold. 그리고 목록이 될 필요는 없습니다. 비어 있거나 비어 있지 않은 것으로 표현할 수있는 모든 컬렉션이 할 것입니다. 기본적으로 모든 반복자가 수행한다는 것을 의미합니다. (저는 "catamorphism"이라는 단어를 던져서 초보자를 소개하기에는 너무 많을 것 같습니다 :-D)
Jörg W Mittag

1

기본 제공 기능으로는 해결할 수없는 좋은 문제를 찾기가 어렵습니다. 그리고 그것이 내장되어 있다면, 언어 x에서 좋은 스타일의 예가되어야합니다.

예를 들어 haskell에서는 (^)Prelude에 이미 기능 이 있습니다.

또는 더 프로그래밍 방식으로 수행하려는 경우 product (replicate y x)

내가 말하는 것은 당신이 제공하는 기능을 사용하지 않으면 스타일 / 언어의 장점을 보여주기가 어렵다는 것입니다. 그러나 장면 뒤에서 어떻게 작동하는지 보여주는 좋은 단계 일 수 있지만 사용중인 언어에 관계없이 가장 좋은 방법을 코딩 한 다음 필요한 경우 진행 상황을 이해하도록 도와야한다고 생각합니다.


1
이 답변을 다른 답변에 논리적으로 연결하려면 곱셈을 함수로, 1을 초기 인수로 사용 product하는 바로 가기 함수 이며 반복자 (또는 목록을 생성하는 함수)입니다. 위의 두 개는 본질적으로 haskell과 구별 할 수 없습니다.) 주어진 수의 동일한 출력을 제공합니다. 이 구현이 위의 @Jack의 답변과 동일한 방식을 이해하는 것은 이해하기 쉬울 것입니다. 단순히 동일한 함수의 사전 정의 된 특수 사례 버전을 사용하여 더 간결하게 만듭니다. foldreplicate
페리아 타 ​​브레 타
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.