[haskell] 함수-2 (순수함수란?)

- 8 mins

하스켈 함수2


함수형 프로그래밍은 순수함수를 조합해서 만든다는 말을 들어본적이 있다.

그렇다면 순수함수가 무엇인지 알아보자.


순수함수


순수함수의 예제는 다음과 같다.

이번에도 역시 일단 개념은 파이썬으로 알아보자.

def sum_one_plus(x, y):
    return x + y + 1

숫자 두 개를 받아 이를 더하고, 추가적으로 1을 더해주는 함수이다.

이는 x의 값과 y의 값이 같은한, 항상 같은 결과를 리턴해주는 순수함수이다.

sum_one_plus(2, 3)  # 6

...

sum_one_plus(2, 3)  # 6

함수의 호출 시점과 상관없이 2와 3이라는 숫자를 넣어주면 항상 6이라는 값을 리턴해준다.

즉, “순수함수란, 같은 입력에 대해 항상 같은 결과를 보장해주는 함수이다”


not 순수함수


함수의 호출 시점과 상관없이,

같은 입력에 대해 항상 같은 결과를 보장하는것이 순수함수라고 했다.

입력이 같음에도 불구하고 함수의 호출 시점에따라 결과가 바뀌는 경우도 있나?

다음 예제를 보자.

n = 1
def sum_n_plus(x, y):
    return x + y + n

이번에는 두 숫자를 더해 n 이라는 변수의 값을 더해준다.

선언 형태만 봐도 이해가 되겠지만,

실제 예제를 한 번 보자.

sum_n_plus(2, 3)  # 6

n = 5

sum_n_plus(2, 3)  # 10

n = 10

sum_n_plus(2, 3)  # 15

분명히 인자로는 2와 3이라는 똑같은 숫자를 넘겨줬는데, 결과가 항상 다르다.

중간에 n이라는 값이 바뀌면서 함수의 결과도 달라진 것이다.

즉, 외부 변수에 의해 결과 값이 달라지는 경우는 순수함수가 아니다.


이런 경우도 생각해볼 수 있다.

n = 10
def sum_one_plus(x, y):
    n = 1
    return x + y + n

실제로 이런 코드를 짜진 않겠지만, 이해를 위해 일단 보자.

이번에도 두 매개변수의 합에 1을 더해 리턴해주는 함수이다.

함수 호출시마다 n이라는 값을 1로 변경 후, 그 값을 더하고있다.


이 경우에는 n을 아무리 바꿔도,

같은 입력에 대한 함수의 결과 값은 항상 같을 것이다.

예제를 보자.

sum_one_plus(2, 3)  # 6

n = 5

sum_one_plus(2, 3)  # 6

n = 10

sum_one_plus(2, 3)  # 6

중간에 n을 바꿔주고 있음에도,

함수 내부에서 n을 다시 1로 재조정 해주고 있으므로 결과는 항상 같다.


앞서

“순수함수란, 같은 입력에 대해 항상 같은 결과를 보장해주는 함수이다”

라고 했으니, 이것도 순수함수일까?

하지만 이것은 순수함수가 아니라는 점에 주의하자.


순수함수는 같은 입력에 대해 같은 결과를 보장해야하는 것도 있지만,

또 한가지 중요한 특징이 있다.

“순수 함수는 외부의 변수에 영향을 미쳐서도 안된다.”

왜냐면 이는 본인에게는 편할 수 있어도,

해당 변수를 사용하는 다른 함수들에게 지장을 초래할 수 있기 때문이다.

혼자 살자고 다같이 쓰는 변수를 바꾸다니! 순수하지 않다.


이처럼 힘수가 동작하면서 프로그램의 특정 상태에 영향을 미치는 경우,

이를 부작용(side-effect)이라고 부른다.

즉, 순수함수를 한마디로 정리해보면,

“동일한 입력에대해 항상 같은 값을 반환하며, 부작용이 없는 함수”

라고 할 수 있다.


기억하기 편하게 한 번 더 정리해 보자면,

“부작용에 영향을 받지 않으며, 부작용을 만들어내지도 않는 함수”

정도로 정의할 수 있겠다.


주의해야할건, 순수함수가 아니라고해서 절대 '틀린' 함수가 아니다.

단지 순수함수의 특징이 그렇다는 것 뿐이고,

함수를 통해 외부 환경을 조작해야하는 경우도 많다.


순수함수 IN Haskell


이제 순수함수가 무엇인지는 대충 알 것 같다.

그럼 하스켈에서는 순수함수를 어떻게 다루고있을까?


“하스켈에서는 모든 함수가 순수함수이다”

기본적으로 내장되어있는 함수는 물론이고,

프로그래머가 어떤 방식으로든 순수하지 않은 함수를 만들어 낼 수 없다.


즉, 하스켈의 모든 함수는 입력이 같으면 같은 결과를 돌려준다.

또한 모든 함수는 함수 외부의 프로그램 동작 환경에 영향을 미치지 않는다.

그러므로 프로그래머 입장에서는 부작용에 신경쓰지 않고 로직에 집중할 수 있으며,

여러 쓰레드(혹은 프로세스)가 동시에 발생하는 환경에서도 신경쓸 부분이 확연히 줄어든다.


라고 한다…

정말 그런지 확인해보자.


우선 다음과 같은 스크립트를 작성해봤다.

-- change_variables.hs
n = 10
sum_n_plus x y = x + y + n

n = 20

앞의 예제처럼 x와 y를 받아 두 수의 합에 n을 더해서 리턴하는 함수이다.

ghci에서 불러와보자.

Prelude> :load change_variables.hs
[1 of 1] Compiling Main             ( test.hs, interpreted )

test.hs:4:1: error:
    Multiple declarations of n
    Declared at: test.hs:1:1
                 test.hs:4:1
  |
4 | n = 20
  | ^
Failed, no modules loaded.
Prelude>

하스켈은 변수를 변경할 수 없기때문에,

변수를 변경하는 부분에서 당연히 에러가 발생한다.

하지만 다음의 경우를 보자.

같은 행위를 ghci에서 수행한 결과이다.

Prelude> let n = 10
Prelude> sum_n_plus x y = x + y + n
Prelude>
Prelude> let n = 20
Prelude>
Prelude> n
20
Prelude>

이게 무슨일인가?

10이었던 n의 값을 20으로 변경할 수 있고,

심지어 값도 변경된 20으로 출력된다.


‘변경’이라고 표현하긴 했지만,

이는 당연히 실제 메모리상에서의 값을 변경한 것이 아님을 알고있다.

하스켈에서 변수는 불변이기 때문에, 그냥 n 이라는 변수를 다른 공간에 ‘재정의’ 한 것 뿐이다.


하지만 프로그래머 입장에서는 분명 n이라는 변수의 값이 바뀐것 처럼 보인다.

그렇다면 n이라는 값을 사용해 정의한 함수 “sum_n_plus” 함수의 결과또한 달라지지 않을까?

파이썬 예제를 잠깐 다시 보자.

n = 1
def sum_n_plus(x, y):
    return x + y + n


sum_n_plus(2, 3)  # 6

n = 5

sum_n_plus(2, 3)  # 10

n = 10

sum_n_plus(2, 3)  # 15

n이 변경될 때마다 sum_n_plus 함수의 결과도 바뀐다.

하스켈에선 어떨까?

최대한 비슷한 에제를 실행해보자.

  1 Prelude> let n = 1
  2 Prelude> let sum_n_plus x y = x + y + n
  3 Prelude> sum_n_plus 2 3
  4 6
  5 Prelude> let n = 5
  6 Prelude> n
  7 5
  8 Prelude> sum_n_plus 2 3
  9 6
 10 Prelude> let n = 10
 11 Prelude> n
 12 10
 13 Prelude> sum_n_plus 2 3
 14 6
 15 Prelude>

분명 n의 값이 변경되는 것처럼 보이지만,

파이썬과 달리 실제 함수의 결과는 항상 동일하게 나온다!

설명의 편의를 위해 라인 번호를 붙였다.

(line 2) x, y를 인자로 받아서 x + y + n 을 결과로 돌려주는 함수를 정의했다.

(line 3) 정의했을때 n의 값은 1이었으므로 함수의 결과는 당연히 2 + 3 + 1인 6이 나온다.

(line 5~7) 이 부분에서 n의 값을 재정의했다. 출력해보니 5로 바뀌어있는것도 확인했다.

(line 8) n의 값이 재정의되었음에도 불구하고 함수는 여전히 같은 결과, 6을 내뱉고있다!

(line 10~12) 이번에도 n의 값을 10으로 재정의하고 출력했다. 10이라는 숫자가 출력된다.

(line 13) 하지만 여전히 함수의 결과는 6으로, 변함이 없다!


그럼 이렇게 추측해 볼 수 있다.

“함수는 자신이 정의되었을 당시 n의 값을 기억하고 있으므로, 이후 n의 값이 어떻게 되어도 신경쓰지 않는다.”

음.. 이건 마치 클로저의 리턴된 내부함수가 외부함수의 context를 기억하는것과 비슷한 맥락인 것 같다.

그러니까 파이썬에서는 함수 선언 당시 n이라는 변수를 실제 전역변수인 n으로 인지하는 반면,

하스켈에서는 함수 선언 당시 n이라는 변수가 가지고있는 ‘값’ 자체를 가지고 있다고 보는게 맞을것이다.

이처럼 하스켈의 모든 함수는 순수함수로, 어느 시점에서 실행하든 항상 같은 결과를 뱉는다는 것을 확인했다!


정리


순수함수란 부작용에 영향받지 않으며, 부작용을 만들어내지도 않는 함수이다.

즉, 실행되는 시점에 상관없이 같은 입력에 대해 항상 같은 결과를 돌려준다.

때문에 동시성 환경에서 다른 쓰레드나 프로세스에 영향을 받지 않고 안전하게 실행될 수 있다.

또한, 하스켈에서는 모든 함수가 순수함수이며,

중간에 변수가 재정의되는(변경되는 것 처럼 보이는) 한이 있어도 함수의 결과는 달라지지 않는 것을 확인했다.


순수함수에 대해 어느정도 정리가 되긴 했지만,

여전히 하스켈에서 변수가 ‘변경’된다는 느낌은 지울 수 없다.

대충 감은 오지만 이 현상에 대해 명확히 설명할 수가 없다.

아직 내공이 많이 부족한 것 같다.

더 깊숙히 파보면서 의문을 해소하자.

(혹시 이 부분에 대해 명확히 설명이 가능하신분은, 댓글 달아주시면 커피 기프티콘 쏘겠습니다…!!)




코딩장이

코딩장이

-장이: [접사] ‘그것과 관련된 기술을 가진 사람’의 뜻을 더하는 접미사.

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora