관리 메뉴

HAMA 블로그

함수형 프로그래밍이란 (2편) 본문

소프트웨어 사색

함수형 프로그래밍이란 (2편)

[하마] 이승현 (wowlsh93@gmail.com) 2016. 1. 11. 17:18


함수형 프로그래밍이란? (1편 부작용) 

위의 한주영님의 번역글을 읽어보면 


"모든 입력이 입력으로 선언되고 (숨겨진 것이 없어야 한다) 마찬가지로 모든 출력이 출력으로 선언된 함수를 ‘순수(pure)’하다고 부른다."


이런 내용이 있는데 , 저자는 숨겨진 입력 ㅡ> 부효과(side-cause) 이라고 했는데 ,보여지는 입력 또한 side-cause 가 생깁니다. 컬렉션이 레퍼런스로 인자로 넘어가서 set 되면 말이죠. 사실 좀  아리까리합니다. 아마 멀티쓰레딩에 관해서는 염두를 안한 글이거나 , set  효과는 아예 배제한것 같으니 , 즉 모든걸 다 담은 글이 아닌점을 글 읽는 분들은 참고하십시요.




함수형 프로그래밍이란 (2편 언어에서 조망)


읽기전에 : 본 글은 블로그에서 자신의 생각을 표현한 글로써, 건조하게 문법을 설명한 글이 아니라서 좀 과격하며 

주관적인 생각도 많이 포함되 있음을 염두하고 읽기 바랍니다.

서두


첫번째 포스트에서 나는 함수형 프로그래밍을 정의했었다. 뭐 교과서적이거나 마케팅적인 관점은 아니었지만 일반 프로그래머에겐 쉽게 이해할 수 있는 상식적인 설명이었다고 본다. 어쨌거나 내가 바라는건 개발자들이 통제불능 상태로 자신의 코드를 몰고가기 전에 어떤 부작용이 있을지 고민해 봤으면 하는 바램이다.

자~!  이제  주변에서 사용되고있는  함수형 언어들에 대해 살펴보자. 

(역주 :   side-cause 와 side-effect  는 둘다 부작용으로 번역했고 그 차이는 part1 에서 참고하시구요. 다만 side-cause  경우 괄호안에 표기했습니다. 부수효과라고 번역도 많이들 하는데 너무 약하다고 봅니다. )

함수형 프로그래밍은 .. 가 아니다.


map 이나  reduce 가 아니다.

모든 함수형 언어에서 저것을 보았더라도, 저것이 언어를 함수형으로 만드는게 아니다.  단지 어떤 시퀀스 요소들에 대해서 작업할때 , 부작용을 없애기위한 노력의 산물일 뿐이다.

(역주: 전 반대로 생각해서 부작용이 없는 코드를 짜야 저런 함수합성요소들을 편히 사용 할 수 있게 됩니다.


람다 (lamda)  함수가 아니다.

일급함수에 대해 언급하는 것을 모든 함수형 언어에서  들었을 것이다. 그러나 그것은 부작용을 피하는 언어를 만드는것을 시작할때 자연스럽게 도출되는 것이다.  도와주는 요소이지 근본은 아니다. 


타입문제가 아니다.

정적 타입 검사방식은 매우 유용한 도구이다. 그러나 그것이 함수형 프로그래밍 (FP) 의 선행 요구사항은 아니다. Lisp 은 오래된 함수형 언어이자, 가장 오래된 동적 언어이다. 

정적 타입들은 매유 유용하게 될수있다. 헤스켈은 그것의 타입 시스템을 부작용을 처리하기위해 아름답게 이용한다.  그러나 함수형 언어를 만들기 위한  재료는 아니다. 

다시한번 강조하자면 !!    함수형 언어는 부작용에 대한 것이다. 


각각의 언어에서 그 의미는 무엇인가?


자바스크립트는 함수형 언어가 아니다.

함수형 언어는 당신이 컨트롤 할수있거나, 할수없는 모든곳에서 부작용이 일어나는것을 제거하는데 도움을 주는 언어이다. 자바스크립트는 이런 기준에 미흡하며  사실 자바스크립트에서 부작용을 조장하는 지점은 쉽게 찾을수있다.

가장 찾기 쉬운 부분은  this이다. 그 숨겨진 input 은 모든 함수에 존재한다.   this  에 존재하는 마법같은 일들은 주로 자신의 의미가 쉽게 바뀐다는데 있는데 , 심지어 자바스크립트 전문가들 조차도 이놈의  this 가  무엇을 가르키고 있는지 추적하는데 쩔쩔 맨다는 점이다.  함수적 관점에서 보면 이 모든 마법적으로 활용되는 일들에서 불쾌한 냄새가 나는듯 하게 느껴진다.

자바스크립트에 함수형 헬퍼 라이브러리들을 로드할수있는데  ( 예를들어 Immutable.js ) , 그것은 자바스크립트를 함수형 스타일로 개발하는데 더욱 쉽게 만들어준다. 물론 언어 그 속성 자체를 바꿀수는 없지만..  


자바는 함수형 언어가 아니다.

자바는 확실히 함수형 언어가 아니다. 자바 1.8 에서의 람다 기능의 추가는 그것에 전혀 영향을 미치지 않는다.  (역주: 람다 와 모나드 (스트림API) 를 통해서 부작용을 해소하려는 시도는 있습니다.) 자바는 함수형 프로그래밍의  반대 방향에 꿋꿋히 서있는다. 자바의 핵심 디자인  설계자는 "코드는 부작용을 일련의 지역화로  다루어야한다 " 라고 말하고있다.  메소드들은 객체의 지역 (local ) 상태를 바꾸거나 의존한다.

사실 자바는 함수형 프로그래밍 (FP) 에 적대적이다. 만약 당신이 자바코드를 부작용없이 작성하려면, (객체의 상태를 바꾸거나 읽지 않는 )  당신은 그쪽 세상에선 형편없는 프로그래머라고 불리게 될것이다. 그게 자바가 쓰여지는 방식이 아니니깐.   당신의 부작용 청정 코드는 static  키워드로 양념될것이고 , 그들은 눈쌀을 찌부리며 당신의  책상을 화장실로 옮겨놓을 것이다.  (역주 : 이부분은 조금 이해안가네요. static 을 남발하면 부작용 청정 코드가 되나요?  static 을  immutable 로 사용한다는 전제를 한거 같습니다.

물론  나는 자바가 나쁘다고 말하는건 아니다.  (흠 , 오케이~그렇게 볼수도 있겠다)  , 그러나 요점은 부작용을 보는 관점에서 전혀 다른 시각을 가지고 있다는 점이다. 자바 경우는  지역화된 부작용은 좋은 코드의 주줏돌이라고 보며 ,  함수형 프로그래밍은 그것들을 악이라고 본다. 

당신은  자바나 FP 이 부작용이라는 문제을 대응하는 관점에 대해 양쪽을  다른 각도로  볼수 있을 것이다. 양쪽 모델은 문제로서 부작용을 인지하며 , 다르게 대응한다. 객체지향의 대답은 " 그들을 '객체' 라는 바운더리 내에 포함한다 ' 이고 반면 함수형의 대답은 ' 그들을 제거한다 ' 이다. 운이 없게도 , 실제 자바는 부작용을 캡슐화하는 시도를 하지 않고 그들을 당연하게 관행으로 바라본다. 만약 상태가 있는 객체의 형식에서  당신이 부작용을 만들지 않는다면, , 당신은 결국 나쁜 자바 프로그래머가 될것이다.  사람들은 static을 남발하는 당신을 해고 할것이다. 

역자추가) 

자바 8 의 stream API 는 i/o 에서 사용되는 inputstream / outputstream 과는 완전 다른것이며 
stream API 는 함수형 세계에서 말하는 모나드 입니다. 그래서 자바에서 함수형 프로그래밍의 멋진 부분을 
가지고 놀수있게 된것이죠.스트림 API 는  순서대로 요소를 처리하는 다양한 방법을 제공하며 런타임 성능 향상에 
좋은 영향을 줄수 있습니다.

참고 : JAVA 8 Stream API 

스칼라는 큰 (역주: 허황된 )  과업을 가지고 있다.

생각해보면 스칼라는 매우 도전적인 제안을 하고있다. 만약 스칼라의 목적이 객체지향과 함수형의 두 세계를 묶는것이라면 , 부작용이라는 렌즈를 통해서 보면  " 부작용 의무화"  와   "부작용 금지" 의 차이에 다리를 놓아서 연결하겠다는 것이다.  그들이  조화롭게 뛰어노는 꽃동산을 만든다는것이 가능할지는 모르겠다. 당신이 스칼라를 사용하는데 ,    map함수를 지원하는 객체들을 만드는것에 두 사상을 통합하여 개발  할수 없을것이다. (역주 : 만약 대중화가 된다면 대부분 스칼라를 가지고 자바처럼 코딩 할 것이라 생각듭니다. 제가 쓴 함수형 프로그래밍의 대중화 될수 없는 이유에서  빈약하지만 얘기했듯이,  저는 함수형 언어의 대중화는 불가능이라  생각합니다. ) 

만약 스칼라가 그런 통합에 성공할것인지 판단하는건 각자의 몫으로 남겨둘것이다. 그러나 만약 내가 스칼라의 마케팅 담당자라면 그들을 통합하는 대신해 스칼라를 점진적으로 자바의 부작용으로부터 벗어나서 순수FP 의 세계로 이동하게 도움주는 것으로 홍보하고 싶다. (역주: 스칼라가 비록 하이브리드지만 FP 로만 사용해야 한다고 생각합니다. C++ 가 하이브리드지만 객체지향언어이듯이..


클로저 (Clojure)

클로저는 부작용에 대해 흥미로운 위치에 자리잡고 있는데  그 언어의 창조자 리치 하키는 "클로저는 대략 80% 정도 함수형 이다 " 라고 말하고있다.  나는 왜 그런지에 대해 명확히 말할수있다고 생각하는데,  시작부터 클로저는 부작용에 대한 하나의 특정 종류를 다루는것으로 디자인되있다.  :  시간   

이것을 살펴보자, 여기에 당신을 위한 자바 유머가 있다:

  • 5 더하기 2는 무엇?
  • 7.
  • 정답.  그럼 5 더하기 3은 ? 
  • 8
  • 땡~!  10 이 정답.  왜냐면 우린 5 을 7 로 바꿨거든 기억해?   (역주:  하나도 안웃겨...

오케이~인정. 뭐 훌륭한 조크는 아니었다. 그러나 포인트는 말이지. 자바왕국에서는 values 가 계속 유지되지 않어. 정당하게 5를 표현하는 어떤것을 가질수도 있는데 함수를 호출함으로서 그게 더이상 5가 아님을 알수있게되지.  (역주 :  어떤 값을 가지고 있는 변수/객체를 함수에 인자로 넘기면, 레퍼런스 값이 복사되어 넘어가므로 내부에서 어떤 행위를 하면 본질이 바뀌어 질수 있음을 말함. Call by Reference value 즉 너무 당연하게 생각해왔던것이 사실 당연한게 아니었던것이다

Integer 경우는 사소한것이고, 커다란 객체로 본다면 그 영향은 크게 증폭된다고 말할수있다. Part 1에서 말한 InboxQueue 를 떠올려보면 InboxQueue 의 상태는 시간에 따라서 달라질수 있는 value 이다. 그래서 결국 시간은 InboxQueue 의 의미에 대한 부작용(side-cause)이라고 말할수있는거지.  

클로저는 그 시간의 부작용 (side-cause) 에 빡세게  촛점을 마추고있다. 시간의 숨겨진 효과 때문에 우리가 저장한 값에 의존할 수 없게 되고, 저장한 값에 의존할 수 없으면 함수의 입력값에도 기댈수 없게 되며, 따라서 우리는 어떠한 것에도 그것이 예상가능하게 혹은 반복적으로 동작할 것이라고 의존할 수 없게 된다는 것이 바로 리치 히키의 통찰이다.  (한주영님 번역으로 수정)

만약  값이 부작용을 가지고있다면,  모든것이 부작용들을 가진다. 만약  값이  순수하지 않다면 우리의 프로그램안에는 어떠한 순수한것도 남아있지 않게 된다. 

그래서 클로저는 시간에 대한 무기를  갖는다. 모든 value 는 디폴트로 불변형 (immutable) 이다. (시간이 지나도 변하지 않음) . 만약  value  를 바꾸길 원할 경우에  클로저는 바뀌지 않는 값에 대한 래퍼를 제공하며 그 래퍼는 무거운 제약들을 갖는다. 

  • 래퍼를 사용해서 값을 바꾸기위해  강제적으로 사전동의 (opt-in)  를  구해야한다. 
  • 일시적이거나 자연스럽게 변형가능한 value 를 만드는것을 할 수 없다. 항상 언어에서 마련한 명시적인 플레그 (부작용에 대한 보호막 ) 를 이용해야한다.   
  • 자신도 모르게 변형가능 값을 소비할수없다. 항상 언어적인 보호장치를 사용해야한다. 
  • 변경가능 값 래퍼를 사용하고 , 다시 돌려 받을때는 불변형이다. 쉽게 시간-의존적인 세계 밖으로 순수한 녀석을 되돌려 받을 수 있다. 

시간 관점으로,  클로저는 매우 훌륭한 함수형 프로그래밍 언어의 예이다. 시간의 부작용에 대해 깊이 있는 적대심을 표현하고 있다. 어디에 있든지 제거하며 (디폴트로)  , 당신을 도와줄것이다. 


하스켈 (Haskell)

만약 클로저가 시간이라는 특성에  반대한다면 ,  하스켈은 평범히 (?)  부작용에 대처하는 느낌이다. 하스켈은 정말 부작용을 싫어라 하는데,  많은 노력을 그것을 컨트롤링하는데 쏟아 부었다. 하스켈이 부작용에 항거하는 흥미로운 방식중 하나는 타입들과 연관된다. 타입시스템 안에 부작용들을 모두 푸쉬 하는데 예를들어 getPerson 함수를 가지고 있다고 치고 ,  하스켈에서는 다음과 같다: 


getPerson :: UUID -> Database Person

"UUID 를 이용하여  Database 의 문맥안에서 Person 을 리턴하라" 라고 읽을 수 있다. 이건 흥미로운데 - 당신은  하스켈 함수의 타입 시그너쳐를  바라 볼 수 있으며,  어떤 부작용을 포함하고 그렇지 않은지에 대해 알수있다.  ( 역주 : 하스켈을 공부하면  알수있겠지...)  그리고 다음과 같이 보장 할 수 있다.  " 이 함수는 파일 시스템에 접근하지 않을겁니다. 왜냐하면 부작용의 종류로서 선언되지 않았어요".   즉 명시적인 타이트한 컨트롤을 한다. 

중요한 포인트는 또 있는데, 함수를 다음처럼 볼수있다.

formatName :: Person -> String

... 그리고 이것이 Person 을 가지고 String 을 리턴하는지 알 수 있다. 그 밖에 다른게 전혀없다. 왜냐하면 만약 부작용이 있다면, 당신은 타입 시그니쳐에 그것들이 가둬지는 것 (역주 : 하스켈에서는 아마 명시적으로 표현되나 보다) 을 확인할수 있기 때문이다.  아마도 가장 흥미로운것은 다음 예인데: 

formatName :: Person -> Database String

이 시그니쳐는 우리에게 formatName 의 이 버전은 데이타베이스 관련 부작용을 포함한다고 말하고 있다.  " what the hell ?? 왜 formatName 이 데이터베이스를 필요로 한거야? "  당신은  " 나는 셋업(set-up) 이나 목아웃(mock-out) 할 예정이야 단지 그 함수에 대해서 테스트 해 볼 건데.. 이 무슨.. "  라고 생각 중인데,  저건 정말 이상하다. 이 함수 시그니처를 보면 , 나는 디자인적으로 먼가 잘못된것이 보인다. 나는 코드를 볼 필요가 없이 썪은 냄새를 맡을 수 있다.  

자바의 함수 시그니쳐와 간단히 비교해보자:

public String formatName(Person person) {..}

어느 하스켈 버전이 저것과 동등한가?  자바의 경우 함수의 몸통을 조사할 필요도 없이, 당신은 저것에 대해 알 도리가 없다. 그것은  데이타베이스에 아마 접근할수도 있고  또는  파일들을 제거하고 리턴할것이며 선임을 빡치게 만들것이다.  그 타입 시그니처는 당신에게  무엇이 작동하는지에 대해 아주 조금  말한다.  

하스켈의 타입 시그니처는 ,  자바와  대조적으로 , 당신에게 디자인을 멋지게 다루는것에 대해 말한다. 그리고 그들은 컴파일러에 의해 체크되기 때문에, 당신이 아는게  맞다고 말한다. 그것은 훌륭한 아키텍처 도구들을 만든다는것 의미한다.  

하스켈은 매우 높은 레벨의 디자인 향기를 표면화 한다.  또한 코딩의 패턴 또한 표면화 한다. 나는 "함수자 (functor) " 라든지 "모나드(monad)" 라는 단어를 이번 포스트에서 제외할것이나  높은 수준의 소프트웨어 패턴들은 높은 레벨의 분석과 함께 시작한다고 말할것이다. 높은 레벨의 분석은 당신이 높은 레벨의 표기법(notation) 을 가질때 더욱더 쉽게 만들어진다.   


파이썬(Python)

근본적인 부작용에 대해 자바를 이용해서 빠르게 살펴보자. 

public String getName() {
  return this.name;
}

어떻게 우리는 이 함수를 순수하게   할까?  this  는 위에 말했다시피 숨겨진 입력이고, 우리가 해야할것은 그것을 매개변수로 올려놓는것이다:


public String getName(Person this) {
  return this.name;
}

지금 getName 는 순수한 함수이다. 기본적으로 파이썬이 이러한 패턴을 채택하고있다는것을 말하려는것인데. 파이썬에서 또한 모든 객체 메소드들은 this  를  숨겨진 첫번째 인자로 가진다.  self: 로 불려질때를 제외하고

def getName(self):
    self.name

명시적인게 암시적인거보다 훨씬 좋다. 정말로~


인자가 없다는건 부작용(side-cause) 의 신호이다. 

당신이 인자가 없는 함수를 볼때마다, 두가지 중 하나는 사실이다.  정확히 동일한 값을 리턴한다. 또는 어디에선가로 부터 입력을 받는다. (즉. 부작용을 가진다) 

예를들어, 이 함수는 틀림없이 항상 동일한 정수를 리턴한다. ( 또는 부작용을 가진다 ) 

public Int foo() {}


리턴 값이 없다는건 부작용(side-effect)의 신호이다

 리턴값이 없는 함수를 볼때마다 , 그건 부작용이 있거나 그것을 호출하는 포인트가 없다는것을 얘기한다.

public void foo(...) {...}

저 함수의 시그니처에 따르면,  이 함수를 호출할 이유가 전혀 없다. 아무것도 당신에게 해주질 않는다. 이것을 호출하는 유일한 이유는 마법적인 부작용을 아주 조용히  일으키기 위해서이다. 


요약 

부작용에 대한 직관적인 인식은 당신이 코딩을 바라보는 방식을 바꿔줄것이다. 사소한 함수에서부터 시스템 아키텍쳐의 전반적인 사항을 조망하는것까지  많은것을 바꿀것이다. 또한 툴이나 테크닉에서 부터 언어를 바라보는 시야 또한 바꾸고 넓힐것이며. 결국 모든것을 바꿀것이다. 

자~ 이제부터 부작용을 죽이는 네팔렘이 되어서,  소프트웨어 대균열에 맞서 보자. 



Comments