본문 바로가기

날리지/언어

함수형 프로그래머가 되기 위한 ... ... (1)

함수형 프로그래밍

 

함수형 프로그래밍의 개념을 이해 하는 것은 제일 중요하지만 상황에 따라서는 또 가장 어렵고 난해한 부분이 아닌가 싶다. 그러나 다른 쉬운 관점에서 보면 꼭 그런 것만은 아니다.

운전을 생각해 보자. 우리는 좀 당황하고 있는 자신을 발견하게 될 것이다. 다른 사람이 운전하는 모습을 보면 그다지 어려운 것 같지 않아 보이지만 말이다. 그러나 배우기 시작하면 우리가 생각하던 것보다 더 어려운 면이 있음을 발견하게 될것이다. 우리 부모님의 차를 운전하며 동네의 익숙한 골목길을 마스터 하기 전까지 절대로 고속도로 진입은 시도하지 않을 것이다. 그러나 수많은 반복과 패닉을 가져다주는 순간을 거쳐서 마침내 운전면허를 따게 될 것이다.

그리고 운전을 하다 보면 운전 실력이 나날이 향상되는 자신을 발견하게 될 것이고 마침내 새로운 자기의 차도 마련하게 될 것이다. 새로운 차를 마련하게 되면 이전과는 다른 질문과 생각을 가지게 될 것이다. 차의 차키는 어디에 넣지? 비상등 버튼은 어디지 ? 등등 ... 그러나 처음 차를 탈 때와는 달리 아주 스무스하게 새로운 차에 적응하고 운전하게 될 것이다.

왜 그럴가? 왜냐하면 두번째 차는 본질 상에서 첫 번째 차와 같고 그 기능면에서도 동일하며 다만 비상등 버튼의 위치나 이런 세부적인 면에서 차이가 조금씩 있기 때문이다.

프로그래밍 언어를 배우는 것도 이와 마찬가지이다. 처음 것은 어려우나 그것을 마스터하게 되면 뒤의 것은 처음 것만큼이나 어려운 것은 아닐 것이다.

그러나, 이번엔 우주선에 오른다고 생각해보자. 우주선을 운전하는 데 차를 운전하던 경험이 크게 도움이 될 것 같지 않다. 우리는 제로 베이스에서부터 시작해야 될 것이다. 우주선을 운전하는 것은 땅에서 자동차를 운전하던 경험과 사뭇 다르다.

프로그래밍과 함수형 프로그래밍의 관계는 바로 이런 관계이다. 완전히 다른 사고체계이다. 그러므로 완전히 새로운 사고체계를 받아들여야 할 때이다.

"Learning functional programming is like starting from scratch"

단순히 모든 것을 다시 새롭게 배운다고 생각하면 마음이 편할 것이다. 이것이 우리가 생각하는 올바른 관점이다.

아래와 같이 아주 정직한 자바스크립트 함수가 있다고 가정하자.

var z = 10;
function add(x, y) {
    return x + y;
}

add 함수는 변수 z를 건들이지 않을 것이다. 다만 파라미터로 받아들이는 x와 y의 값을 더한 다음 그 결과를 반환하는 작업을 수행할 뿐이다. 이는 순수한 함수 (Pure Function) 이다. 그러나 이 함수가 변수 z를 건드리는 순간 이 함수는 더 이상 순수하지 않게 된다.

아래와 같은 함수를 보자.

function addNoReturn(x, y) {
    var z = x + y
}

이 함수는 순수한 함수이다. 파라미터로 받아들이는 함수만 건드리기 때문이다. 하지만 아무런 결과 값을 반환하지 않음으로 아무데도 쓸모 없는 함수가 된다. (Useless function)

첫 번째 add함수를 다시 살펴보자

function add(x, y) {
    return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3

함수는 1과 2를 더한 결과를 출력한다. 우리는 1과 2를 더한 결과 값이 3임을 기대할 수 있다. 하지만 이는 이 함수가 순수형 함수라는 전제 아래에 그런 것이다. 파라미터인 1과 2를 받아서 1과 2만 가지고 연산을 하기 때문에 다른 결과를 기대할 수 없다. 하지만 함수가 함수 밖의 다른 변수를 건드린다고 한다면 그 결과를 영원히 예측할 수 없을 것이다. 즉 순수형 함수는 같은 파라미터를 입력하면 영원히 같은 결과 값을 반환한다.

순수형 함수는 함수 밖의 그 어떤 변수 값도 변경하지 않음으로 아래에 나열 된 함수들은 모두 순수형 함수가 아니다.

writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

위의 함수들은 모두 부수효과가 있다. 함수를 호출하면 함수 밖에서 무슨 일을 하게 되는 것이다. 파일을 변경하거나, 데이터 베이스 테이블을 변경하거나, 혹은 데이터를 서버로 전송한다든지 등등 일이 함수 밖에서 일어난다. 이것은 단순히 파라미터를 받아서 처리하여 반환하는 차원의 프로세스가 아닌 것이다. 그러므로 함수가 어떤 것을 반환할 지 도무지 예측하기 어렵다.

"Pure functions have no side effects."

Javascript, Java 및  C#과 같은 언어들에서 이런 부수효과를 가진 함수들을 도처에서 볼 수 있다. 이는 디버깅을 어렵게 하는 결과를 초래하게 된다. 변수가 프로그램의 많은 부분에서 그 값이 변경될 수 있기 때문이다. 때문에 변수의 값이 틀린 시간에 틀린 값으로 변경이 되었다면 어디를 봐야 하는가? 이는 좋지 않은 결과를 가져올 수 있다.

이럴 때면 당신은 이런 생각을 하게 될 것이다. 

"순수형 함수로만 구성된 프로그래밍을 할 수 없을가?"

"순수형 함수로만 구성된 프로그래밍을 할 수 없을가?"

함수형 프로그래밍에서는 단순히 순수형 함수로만 만들 수 없다. 프로그래밍에서 부수효과가 없을 수 없다. 다만 그들을 제한할 뿐이다. 그러므로 함수형 프로그래밍의 목표(goal)는 순수하지 못한 함수를 최대한 줄이고, 순수한 코드들로부터 이들을 분리시키고 차별화시키는 것이다.

var x = 1;
x = x + 1;

위와 같은 함수식을 본 기억이 있는가?

 x+1 의 값의 결과를 다시 변수 x 에 대입하라는 것이다.

그러나, 함수형 프로그래밍에서 x = x + 1 이렇게 선언하면 안된다. (Illegal)

"함수형 프로그래밍에는 변수가 없다."

값을 저장하는 변수는 역시 변수라 불리우지만 이 변수의 값을 변경하는 일은 일어나지 않는다. 변수에 한번 값이 지정되었으면 영원히 그 값만 가지게 되는 것이다.

아래의 Elm 코드를 보기로 하자. Elm은 웹 개발을 위한 순수 함수형 프로그램 언어이다.

addOneToSum y z =
    let
        x = 1
    in
        x + y + z

함수 addOneToSum에서는 파라미터 y 와 z 를 받아서 함수 내부에서는 지역변수 x 를 정의하고 1 을 변수에 대입한다. 그러면 이 변수의 값은 영원히 1 이 되는 것이다. 그러므로 이 함수는 정확하게 1 + x + z 의 값을 반환하게 된다.

그러면 당신은 이렇게 말할 것이다.

"어떻게 변수가 없이 프로그래밍을 할 수 있다는 말인가?"

자, 그러면 변수의 값을 변경하는 두 개의 예상 가능한 시나리오를 보기로 하자.

함수형 프로그래밍은 값이 변경된 레코드 사본을 작성하여 레코드의 값 변경을 처리한다. 이를 가능하게하는 데이터 구조를 사용하여 레코드의 모든 부분을 복사하지 않고도 효율적으로 수행한다. 함수형 프로그래밍은 단일 값 변경을 복사하여 동일한 방식으로 해결한다.

루프가 없이 이를 해결 한다는 말이다.

오, 그런가? 변수도 없고 이제는 루프도 없다고 ???

이는 for, while, do, repeat, 와 같은 루핑 도구가 없다는 것을 의미한다.

"함수형 프로그래밍은 재귀를 이용하여 루핑 한다."

자바 스크립트에서 두 가지 방법을 이용하여 루핑을 할 수 있다.

// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
console.log(acc); // prints 55// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
    if (start > end)
        return acc;
    return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55

Elm에서 어떻게 루핑을 하는 지 살펴보자

sumRange start end acc =
    if start > end then
        acc
    else
        sumRange (start + 1) end (acc + start) 

내부적으로 돌아가는 부분이다.

sumRange 1 10 0 =      -- sumRange (1 + 1)  10 (0 + 1)
sumRange 2 10 1 =      -- sumRange (2 + 1)  10 (1 + 2)
sumRange 3 10 3 =      -- sumRange (3 + 1)  10 (3 + 3)
sumRange 4 10 6 =      -- sumRange (4 + 1)  10 (6 + 4)
sumRange 5 10 10 =     -- sumRange (5 + 1)  10 (10 + 5)
sumRange 6 10 15 =     -- sumRange (6 + 1)  10 (15 + 6)
sumRange 7 10 21 =     -- sumRange (7 + 1)  10 (21 + 7)
sumRange 8 10 28 =     -- sumRange (8 + 1)  10 (28 + 8)
sumRange 9 10 36 =     -- sumRange (9 + 1)  10 (36 + 9)
sumRange 10 10 45 =    -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 =    -- 11 > 10 => 55
55

당신은 for 루프가 더 낫고 직관적이라고 할 수 있을 것이다. 그러나 이는 단순히 당신이 for 루프에 익숙해져 있기 때문일 지도 모른다. 어느 것이 더 낫다는 것을 결정하기는 어려운 일일 것이다.

나는 변수의 불면성에 대해서 많이 이야기 하지 않았지만,  Scalfani의 Why Programmers need Limits를 읽어보기를 바란다.

이런 방법의 좋은 점은 한 변수가 생성되면 그 변수의 값을 영원히 변하지 않음으로 예측가능하다는 데 있다.

멀티 쓰레드에서도 다른 쓰레드에서 그 값을 변경하려고 한다면 변수의 값을 변경하는 것이 아니라, 새로운 변수를 만드는 것이다.

"불변성이 더 심플하고 안전한 코드를 만든다. "

from : https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536

 

So You Want to be a Functional Programmer (Part 1)

Taking that first step to understanding Functional Programming concepts is the most important and sometimes the most difficult step. But it…

medium.com