스크랩: https://medium.com/@jooyunghan/%EC%96%B4%EB%96%A4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4%EB%93%A4%EC%9D%B4-%ED%95%A8%EC%88%98%ED%98%95%EC%9D%B8%EA%B0%80-fec1e941c47f

(번역) 어떤 프로그래밍 언어들이 함수형인가?

이 글은 Kris Jenkins(@krisjenkins)의 “Which Programming Languages Are Functional?”을 허락을 구해 번역한 것입니다. (@bradlee 님의 번역도 있습니다.)

이 글의 1부에 해당하는 “What is Functional Programming?”을 번역한 글은 “함수형 프로그래밍이란 무엇인가?”입니다.


작성일: 2015년 12월 29일

이 글의 1부에서 나는, 학문적이거나 마케팅적인 관점을 떠나 평범한 프로그래머가 이해할 수 있도록 함수형 프로그래밍을 정의했다. 사실 그보다 더 중요한 것은 부작용(혹은 부대작용, side-effect)이 무엇인지 이해하고 그것이 통제를 벗어나기 전에 발견할 수 있게 한 것이다(적어도 그랬기를 바란다).

이제, 현실 세계에서 함수형 프로그래밍 언어를 찾아 보자…

프로그래밍 전반에 걸쳐…

부작용을 발견할 수 있는 눈을 가졌으니 주어진 함수에 숨어있는 복잡성도 발견할 수 있게 됐다. 현실적인 FP정의를 알았으니, 그것을 기반으로 프로그래밍 세상 전방위에 걸쳐 새로 획득한 통찰을 발휘해보자.

함수형 프로그래밍은 …이 아니다

map이나 reduce가 아니다

모든 함수형 언어에서 이 함수들을 보게 된다 할지라도 이것으로 인하여 어떤 언어가 함수형이 되는 것은 아니다. 시퀀스 자료를 처리하는 과정에서 부작용을 걷어내려고 노력하다보면 언제나 얻게되는 함수들일 뿐이다.

람다 함수가 아니다

역시나 여러분이 보게 될 모든 FP 언어에는 일급 함수(first-class function)가 있을 것이다. 람다 함수 역시 부작용을 회피하기 위한 언어를 만들다보면 자연스럽게 얻어지는 것이다. FP를 가능하게 하는 것이지 핵심 요소는 아니다.

타입에 관한 것이 아니다

정적 타입 검사는 매우 유용한 도구이지만 FP의 전제 조건이 아니다. Lisp은 가장 오래된 함수형 프로그래밍 언어이자 가장 오래된 동적 언어이기도 하다.

정적 타입이 매우 유용할 수는 있다. 하스켈은 타입 시스템을 사용하여 아름답게 부작용을 공격한 케이스다. 그러나 정적 타입이냐 아니냐가 함수형 언어이냐 아니냐를 가르지는 못한다.

큰 소리로 길게 말해보자. 함수형 프로그래밍은 부작용에 관한 것이다.

(물론 부원인도 포함한다)

언어 차원에서 의미하는 바는 무엇인가?

자바스크립트는 함수형 프로그래밍 언어가 아니다

함수형 언어는 여러분이 부작용을 제거할 수 있는 곳에서는 제거를 돕고, 그럴수 없는 곳에서는 통제할 수 있게 도와준다. 자바스크립트는 이 기준을 만족하지 않는다. 사실 자바스크립트가 적극적으로 부작용을 장려하는 것은 쉽게 찾을 수 있다.

가장 쉽게 찾을 수 있는 것이 바로 this이다. this는 모든 함수의 숨겨진 입력이다. 특히 this가 마법처럼 보이는 까닭은 의미 변화가 자유롭기 때문이다. 심지어 전문가라고 할만한 자바스크립트 프로그래머들조차 this의 참조 대상을 추적하는데 어려움을 겪는다. 함수형 관점에서 보자면 어디서나 마법처럼 접근 가능하다는 사실이 설계 결함의 징후(design smell)이다.

여러분은 분명 FP 라이브러리(예를 들어 Immutable.js)를 로드할 수 있고 또 이를 통해 함수형 스타일로 프로그래밍 하기가 더 쉬워지기는 하겠지만 언어 자체의 본질은 바뀌지 않는다.

(여러분이 만약 요즘 자바스크립트 진영에서 인기를 더해가는 함수형 라이브러리들을 좋아한다면 한번 상상해 보라. 언어 자체가 함수형 스타일을 지원한다면 얼마나 좋아질지를…)

자바는 함수형 프로그래밍 언어가 아니다

자바는 확실히 함수형 언어가 아니다. 자바 1.8에서 람다가 추가되었다고 바뀌는 것은 없다. 자바는 함수형 프로그래밍과 정반대 지점에 있다. 자바의 핵심 설계 원칙에서 말하는 것이 바로 “코드는 지역화된 부작용들 — 즉 객체의 지역 상태를 변경하거나 지역 상태에 의존하는 메쏘드들 — 로 조직화되어야 한다”이다.

사실, 자바는 함수형 프로그래밍에 적대적이다. 여러분이 자바 코드를 작성하면서 부작용 없게, 즉 로컬 객체 상태를 읽거나 변경하지 않게 작성한다면, 여러분은 “나쁜 프로그래머”라고 불리게 될 것이다. 자바는 원래 그런 식으로 작성하지 않기 때문이다. 여러분이 작성한 부작용 없는 코드는 static 키워드로 점철될테고 여러분은 잔뜩 화가난 동료들에 의해 자바 동네에서 쫓겨날 것이다.

자바가 틀렸다는 말을 하려는게 아니다. (음, 좋다. 그걸 부정하지는 않겠다) 요지는 부작용에 대해 완전히 다른 관점을 가지고 있다는 점이다. 자바는 부작용을 국소화하는 것을 좋은 코드의 초석이라고 보며, FP는 부작용을 악으로 간주한다.

여러분은 조금 다른 각도에서 볼 수도 있다. 자바나 FP나 부작용의 문제에 대한 응답이라고 볼 수 있다. 두 가지 모델 모두 부작용을 문제라고 인식하는 것은 같지만 반응이 다를 뿐이다. OO의 대답은 “부작용을 ‘객체’라는 경계에 가두어라”이고, 반면 FP의 대답은 “부작용을 제거하라”이다. 안타깝게도 사실상 자바는 부작용을 캡슐화하려는 시도를 전혀 하지 않으며 오히려 그것을 필수화한다. 상태를 가진 객체라는 형태로 부작용을 만들지 않는다면 여러분은 “나쁜 자바 프로그래머”일 뿐이다. 실제로 static을 너무 자주 사용한다고 해고되는 경우도 있다.

스칼라는 큰 짐을 지고 있다

이러한 관점에서 보자면 스칼라는 매우 도전적인 제안이다. 스칼라의 목표가 OO와 FP라는 두 세상을 통합하는 것이라면, 부작용이라는 렌즈로 들여다 봤을 때 스칼라가 “부작용 필수”와 “부작용 금지” 사이의 격차를 해소하려는 것이라고 볼 수 있다.(1) 이 둘은 이렇게나 정반대의 관점을 가지기 때문에 나는 이 둘의 조화가 가능할 것이라고 보지 않는다. 객체에 map 메쏘드를 추가한다고 두 세상이 통합되지는 않는다. 부작용에 관한 상반된 두 입장 사이의 충돌을 해소하려면 더 깊은 고민이 필요하다.

스칼라가 이런 화해에 성공적인지에 대한 판단은 여러분에게 맡기겠다. 다만 내가 만약 스칼라의 마케팅을 담당한다면 스칼라를 이렇게 홍보하겠다. 자바라는 부작용 필수 세상을 떠나 FP라는 순수한 세상으로의 점진적 이동을 도와준다고. 이 두 세상을 통합하는 것이 아니라 이어주는 다리 역할을 할 수 있다고. 실제로, 많은 사람들이 그런 식으로 보고 있다.

클로져

클로져는 부작용에 대해 흥미로운 입장을 취한다. 클로져를 만든 리치 히키(Rich Hickey)는 클로져가 “80% 함수형”이라고 했다. 나는 그 이유를 설명할 수 있을 것 같다. 처음부터 클로져는 ‘시간’이라고 하는 한가지 특정 부작용을 잘 처리하도록 설계되었다.

이를 설명하기 위해 먼저 자바 관련 농담을 하나 살펴보자.

  • 5에 2를 더하면 얼마?
  • 7.
  • 정답. 그럼 5에 3을 더하면 얼마?
  • 8.
  • 아니. 10이다. 방금 5를 7로 바꾸었으니까. 그렇지?

물론 훌륭한 농담은 아니다. 그러나 요점은, 자바 세상에서 값은 그대로 유지되지 않는다. 합당하게 5라고 볼 수 있는 어떤 값이 있는데, 다른 함수를 하나 호출한 다음에는 5가 아닐 수 있다는 것이다. 수학에서 5는 절대 바뀌지 않는다. 새로운 값을 반환하는 함수를 부를 수는 있어도 5라는 값 자체가 바뀌지는 않는다. 자바는 값이 시간에 따라 바뀌긴 하지만 그 변화가 객체 경계로 감싸져 있다면 문제 없다고 말한다.

정수 값을 봤을 때는 대수롭지 않게 보일 수 있다. 하지만 더 큰 값이라면 효과가 증폭된다. 1부에서 예로 다루었던 InboxQueue를 기억하는가? InboxQueue의 상태는 시간이 지남에 따라 변하는 값이다. 우리는 시간(Time)을 InboxQueue의 의미에 대한 부원인(side-cause)이라고 볼 수 있다.

클로져는 시간이라는 부원인에 매우 집착한다. 시간의 숨겨진 효과 때문에 우리가 저장한 값에 의존할 수 없게 되고, 저장한 값에 의존할 수 없으면 함수의 입력값에도 기댈수 없게 되며, 따라서 우리는 어떠한 것에도 그것이 예상가능하게 혹은 반복적으로 동작할 것이라고 의존할 수 없게 된다는 것이 바로 리치 히키의 통찰이다.(2) 값 조차도 부작용을 가진다면 모든 것이 부작용을 가지게 된다. 값이 순수하지 않으면 우리 프로그램에서 어떤 것도 순수할 수 없다.

그래서 클로져는 시간에 칼을 들이댄다. 클로져의 모든 값은 기본적으로 불변이다(시간이 지남에 따라 변하지 않는다). 변하는 값이 필요한 경우를 위해 클로져는 변하지 않는 값의 wrapper를 제공한다. 이러한 래퍼는 무거운 제약사항을 가진다.

  • 변하는 (변경가능한) 값을 사용하기 위해 여러분은 래퍼를 사용하여 분명히 표시해야 한다.
  • 여러분은 실수로 변경 가능한 값을 생성 할 수 없다. 여러분은 잠재적 부작용을 언어가 제공하는 가드를 써서 명시적으로 표시해야 한다.
  • 여러분이 눈치채지 못하고 변경가능한 값을 사용할 수가 없다. 여러분은 반드시 부작용의 위험을 인정하기 위해 명시적으로 언어가 제공하는 가드를 사용해야 한다.
  • 여러분이 변경가능한 래퍼를 열고 꺼낸 값은 다시 변경불가한 값이다. 여러분은 쉽게 시간에 의존적인 세상에서 빠져나와 순수한 세상으로 돌아갈 수 있다.

시간에 관한 한, 클로져는 함수형 프로그래밍 언어의 좋은 예이다. 언어 차원에서 시간 부작용에 매우 적대적이다. 클로져는 기본적으로 제거할 수 있는 시간 부작용을 제거하며, 여러분이 필요하다고 여기는 부작용에 대해서는 그것이 여러분의 나머지 프로그램으로 번져나가지 못하다록 단단히 통제할 수 있게 도와준다.

하스켈

클로져가 시간에 대해 적대적이었다면 하스켈은 그냥 전부 적대적이다. 하스켈은 부작용을 정말 싫어하며 그것을 통제하는데 엄청난 노력를 쏟고 있다.

하스켈이 부작용과 싸우는 흥미로운 방법 중 하나는 타입이다. 하스켈은 모든 부작용을 타입 시스템으로 밀어 올린다. 예를 들어, getPerson 함수가 있다고 가정해보자. 하스켈로는 아마 이렇게 생겼을 것이다.

getPerson :: UUID -> Database Person

“UUID를 받고 Database 컨텍스트에서 Person을 반환한다” 정도로 읽을 수 있다. 여기서 흥미로운 건, 여러분이 하스켈 함수의 타입 시그너쳐만 보고서도 어떤 부작용이 관련되는지 확실히 알 수 있다는 점이다. 그리고 어떤 부작용이 관련되지 않는지도. “이 함수는 타입에 해당 부작용이 선언되지 않았기 때문에 파일 시스템을 접근하지 않는다”라고 보장할 수도 있다. 엄격한 통제다.(3)

마찬가지로 중요한 것은, 여러분이 다음처럼 생긴 함수를 본다면…

formatName :: Person -> String

… 이 함수가 Person을 받아서 String을 반환하다는 것을 알 수 있다. 다른 건 전혀 없다. 만일 이 함수에 부작용이 있다면 타입 시그너쳐에 분명히 표시되어 여러분이 알 수 있기 때문이다.

하지만 아마도 무엇보다도 흥미로운 것은 다음 예일 것이다.

formatName :: Person -> Database String

시그너쳐는 formatName 함수가 데이터베이스 관련 부작용을 포함한다는 걸 알려준다. 이게 무슨? 왜 formatName 같은 함수가 데이터베이스를 필요로 하는거지? 그건 formatName을 테스트하려면 데이터베이스를 셋업하거나 mock을 사용해야 한다는 의미이기도 하다. 이건 정말 이상하다.

함수의 시그너쳐만 보고서도 나는 설계 문제점을 볼 수 있다. 코드를 들여다볼 필요도 없이 나는 개요만 보고서 코드 냄새를 맡을 수 있다. 이건 마법과도 같다.

자바의 시그너쳐도 간단히 비교해보자.

public String formatName(Person person) {..}

하스켈 버전의 두 시그너쳐 중에서 어떤 것이 이것과 동등한가? 함수의 몸체를 들여다보지 않고서 여러분은 알 길이 없다. 순수 버전일 수도 있고, 데이터베이스를 접근할 수도 있다. 아니면 파일시스템을 모두 지워버리고 “팀장, 엿먹어라!”를 반환할 수도 있다. 실제 어떤 일이 일어날지 타입 시그너쳐로 알 수 있는 것이 거의 없고, 함수의 표면이 무엇인지도 알 수 없다.

대조적으로, 하스켈의 타입 시그너쳐는 설계에 관하여 상당히 많은 내용을 말해준다. 그리고 컴파일러에 의해 체크되므로 여러분은 그것이 사실임을 알고있다. 타입 시그너쳐가 훌륭한 아키텍쳐 도구라는 의미이다. 설계 상의 문제를 매우 높은 수준에서 표면에 드러낸다. 코딩 패턴도 마찬가지로 표면에 드러난다. 나는 이 글에서 ‘Functor(펑터, 함자)’와 ‘Monad(모나드)’같은 말을 사용하지 않을 것이다. 하지만 고수준의 소프트웨어 패턴은 고수준의 분석으로 시작되며, 고수준의 분석은 여러분이 고수준의 표기법을 사용하면 훨씬 더 쉽다는 점을 말하고 싶다.(4)

펄(Perl)

부작용에 관한 논의라면 펄 얘기를 안하고 지날 수 없다. 펄에는 마법과 같은 인수 $_가 있는데, 이는 “이전 호출의 반환 값”을 의미한다.(5) 핵심 라이브러리 함수들 중 많은 것들이 이 값을 사용하거나 혹은 변경한다. 묵시적으로. 내가 아는 한 이 기능으로 인해 펄은 단 하나의 전역 부작용이 핵심 기능으로 간주되는 유일한 언어이다.

파이썬

자바에서의 근본적인 부작용 패턴을 잠깐 살펴보자.

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

이 함수를 어떻게 순수하게 만들 수 있을까? this가 숨겨진 입력이므로 인자로 끌어올리기만 하면 된다.

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

이제 getName은 순수 함수이다. 파이썬은 기본적으로 이 두 번째 패턴을 채택하고 있음을 주목하자. 파이썬에서, 모든 객체 메소드는 this를 첫번째 인자로 가진다. 다만 그것을 self라고 부를 뿐이다.

def getName(self):
self.name

분명히 명시적인 것이 묵시적인 것보다 낫다.

Mocking

Mock 프레임워크는 보통 두 가지 일을 한다.

하나는 입력으로 사용할 값 객체를 설정하는 것을 돕는 일이다. 언어가 복잡한 값을 설정하기 어렵게 만들 수록 Mock 프레임워크가 더 유용하게 느껴질 것이다. 하지만 이 얘기는 접어두고..

이 논의에서는 두 번째가 흥미롭다. Mock 프레임워크는 테스트 대상 함수에 대해 부원인(side-cause)을 올바르게 설정하고, 테스트가 실행된 다음 부작용(side-effect)을 제대로 추적하도록 돕는다.

부작용의 렌즈로 보자면 mock은 여러분의 코드가 순수하지 않음을 보여주는 표시이며, 함수형 프로그래머의 눈에는 무언가 잘못되었다는 증명인 셈이다. 맞닥뜨린 빙산을 확인하기 쉽게 도와주는 라이브러리를 다운로드하는 대신 빙산을 우회하여 항해해야 한다.

한번은 하드코어 TDD/자바 개발자가 내게 클로져에서 mocking을 어떻게 하느냐고 물어온 적이 있다. 그 대답은 우리는 일반적으로 mocking하지 않는다 이다. 우리는 대개 그것을 리팩토링이 필요한 신호로 본다.

디자인 냄새 (또는 무의 향기)

부작용에 관한 I-Spy 책이 있다면, 가장 발견하기 쉬운 두 가지 타겟이 바로 인자 없는 함수와 반환값 없는 함수일 것이다.

인자가 없으면 부원인 신호

인자가 없는 함수는 둘 중 하나다. 이 함수는 항상 같은 값을 반환하거나 다른 곳으로부터 입력을 취한다(즉 부원인이 있다).

예를 들면, 다음 함수는 항상 같은 값을 반환하거나 아니면 부원인을 가진다.

public Int foo() {}

반환값이 없으면 부작용 신호

반환값이 없는 함수는 부작용이 있거나 호출해봤자 아무 의미 없는 함수이다.

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

함수 시그너쳐만 보자면 절대 이 함수를 호출할 이유가 없다. 이 함수를 불러봤자 아무 값도 얻을 수 없다. 이 함수를 호출할 유일한 이유는 이 함수가 조용히 발생시키게 될 마법과 같은 부작용 뿐이다.

요약 / 결론(?)

부작용을 실제적이고 직관적으로 인식하는 것은 앞으로 여러분이 코드를 바라보는 시각을 바꿀 것이다. 개별 함수를 보는 것에서부터 전체 시스템 아키텍쳐를 보는 눈까지, 모든 것을 바꿀 것이다. 여러분이 프로그래밍 언어나 도구, 기술을 바라보는 방식을 바꿀 것이다. 그것은 모든 것을 바꿔놓는다. 오늘 당장 부작용을 죽이러 가자!

각주

  1. Java와 OO를 하나처럼 이야기하는데, 맞다. Scala 문맥에서 보자면 이 둘은 동등하다.
  2. 그의 많은 통찰들 중 하나.
  3. PureScript가 이 아이디어를 더 강화했다. 살펴볼만하다.
  4. 나는 클로져 설계와 관련된 논의에서, 설계를 우리 자신에게 설명하고, 설계의 일관성을 검증하고, 결론을 정리하는 과정에서 하스켈시그너쳐를 이용했던 멋진 경험이 있다. 맞다, 클로져 논의였다. 하스켈의 표기법은 언어 차원을 훨씬 넘어서는 가치가 있다.
  5. 정확한 정의는 ‘man perlvar’를 살펴보라.


'Scala' 카테고리의 다른 글

groupby  (0) 2019.02.27
[스크랩]함수형 프로그래밍이란 무엇인가?  (0) 2019.02.26
Part6. Collection) List  (0) 2019.02.25
Part6 Collection) Array  (0) 2019.02.23
Part5 제어문) if문  (0) 2019.02.23