왜 그동안의 함수식 언어 홍보는 잘못됐는가?

그렇다. 함수식 프로그래밍과 그것을 포함하는 “선언식 프로그래밍” [1] 은 그동안 이른바 산업 프로그래밍과 산업 프로그래밍 언어의 혁신요소들의 주요 원천 중 하나였다[2]. 하지만 그동안의 함수식 언어 홍보에서는 중요한 오류들을 범했다.

예를 들어 함수식 에반젤리스트들은 아래와 같은 광고어들을 내세웠었다.

“함수식 언어에서 변수는 값이 바뀔 수 없어요. 그리고 이게 좋아요. 버그를 막아주니까.”

이걸 듣는 사람들의 반응은 아마도 당장 발이 묶인 느낌일 것이다. 그래서 “그러면 코딩을 어떻게 해요?” 라고 묻는다면, 이런 대답이 올 것이다. “변수 변경이 정말 필요할 때는 변경할 수도 있어요. 그리고 함수식 프로그래밍에서는 꼬리 재귀를 사용하지요.” 그러면서 아마도 계승(factorial)이나 피보나치 예제를 주머니에서 꺼낼 것이다.

이렇게 되면 프로그래머는 아마도 고개를 갸우뚱 하며 (말은 하지 않고) 생각할 것이다.

“그렇게 되면 어차피 명령식에서랑 똑같이 변하는 변수를 사용하지 않을까? 과연 참고 변수를 변하지 않게 사용할 수 있을까?”

“그리고 나는 일할 때 매일 계승을 짜진 않는단 말야..흠..”

정리하자면, 대부분 프로그래머들의 일상 코딩 업무에서 주로 다루는 대상은 알고리즘이 아니라, 소프트웨어 시스템의 상태이다. 반복문을 사용하더라도 “루프 불변식(Loop Invariant)”[3][4] 을 명시적으로 확인하고 짜는 경우도 많지 않다. 특히 “모든 것이 웹으로 이동”하는 시대에, 대다수의 프로그래머들이 다루는 소프트웨어는 대부분 아래 두가지 부류에 속한다.

  1. 무상태의 WAS(Web Application Server). 그리고 RDBMS 를 상태 저장 솔루션, 상태 관리 엔진과 트랜잭션 엔진으로 사용한다.
  2. 상태관리가 소프트웨어의 핵심 타스크중 하나인 경우. 예를 들어 MMORPG에서 캐릭터 좌표.

1 의 경우 상태와 “알고리즘” 이 이미 분리되어 있다고 봐도 무방할 것이다.

2 의 경우, “꼬리 재귀”의 반환값으로 상태를 나태내기에는 너무 부적절해 보인다.

Erlang 홍보도 Haskell 홍보와 비슷하지만, 후자보다 조금 나은 점이 있다. 홍보의 후반부로 가면 ETS가 있다고 홍보하기 때문이다. (퀵 앤 더티하게 해석하자면, ETS 는 Erlang 의 In-Process 키-밸류 스토리지 마이크로 서비스(Actor)이다.) (Haskell 에도 이런 유사한 작용을 하는 장치가 존재하는지는 필자도 아직 찾고있는 중이다.)

한마디로 불변성에 대한 어필은 사실상 프로그래머들의 마음을 그다지 움직이지 못했다.

사실 함수식 프로그래밍에서 상태관리는 도대체 어떻게 해야 할것인가 라는 물음을, 하스켈에 갇 재입덕한 필자도 누군가에게 물어보고 싶지만, 물어볼 사람이 없어서 스스로 공부를 더 해 답을 찾아보고자 한다.

“함수식에서는 IO를 격리시켰어요.”

이와 비슷한 광고어는 “사이드 이펙트를 격리시켰어요”이다. 뭐 사실 프로그램에서 사이드 이펙트의 대다수는 IO라고 봐야 할 것이다.

이 광고어도 프로그래머들의 마음을 움직이지 못했다. 우선 “사이드 이펙트” 부터 보자. 이런 반박을 한 프로그래머가 있다.

“사이트 이펙트가 없으면 세상을 개변시키지 않아요.”

다시 IO 격리를 보자. 너무도 핫한 Node.js 를 예로, 얘는 이미 계산과 IO가 격리되어 있다. 계산은 자바스크립트가 하고 IO는 밑단의 Node.js 런타임과 OS 가 한다(실은 하드웨어가 하는데, 일단 OS가 한다고 치자). IO를 런칭하는 코드는 물론 자바스크립트 프로그램속에 섞여있기는 하지만, 자 돌아와서 생각해보자, 애초에 우리는 왜 IO를 격리시키고 싶어했을 까. 음 적어도, 플머들의 IO에 대한 공포심은 도대체 왜 생긴 것일까?

더우기 요즘은 Async/Await 가 대세라, 플머들이 가장 익숙한 동기식 방식의 코드를 짜기만 해도, 마법처럼, 플머도 모르게(Syntax Sugar), Node.js 식의 Continuation Passing Style 로 바꿔준다.

그리고 정말 IO를 분리하고 싶다면 IO만을 독립적인 마이크로 서비스 (또는 Actor)로 빼내는 것도 가능하다.

“Haskell 에는 STM 이 있어요”

이에 대해서는 2편 <왜 STM 시대는 지각하고 있을까?> 예서 다룰 예정이다.

Conclusion

그러면 어떻게 홍보해야 할까? 필자도 잘 모른다. 적어도 필자가 스스로에게 Haskell 을 잘 가르치고 나서 다시 이 질문으로 돌아와야 할 것 같다. 하지만 이번 Haskell 재입덕을 계기로, 기존에는 왜 함수식에 끌리지 않았을까, 란 질문을 스스로에게 한 결과를 공유하고자 했다.

References

[1] 이에 대해서는 쟁점이 존재하는 것 같다.

[2] 2016회고: 같은 성형외과 나온 프로그래밍 언어들: https://coolspeed.wordpress.com/2017/01/02/2016_programming_languages/

[3] 루프 불변식: http://m.blog.naver.com/ctpoyou/104399001

[4] Loop Invariant: https://en.wikipedia.org/wiki/Loop_invariant

Advertisements

One thought on “왜 그동안의 함수식 언어 홍보는 잘못됐는가?

  1. 상당히 중요한 부분을 짚어내셨다고 생각을 합니다. Immutable(불변성)이라는 말 자체가, 많은 입문자들에게 억압적인 느낌을 갖게 한다는 점이 있습니다. 이러한 불변성은 말 그대로 상태를 활용하지 않고 프로그래밍한다기보다는, 기존의 메모리에 쓰여있는 상태가 변하지 않는다는 것으로 이해해야 합니다.

    기존의 것을 건드리지 않고, 정확히는 그것을 토대로(map과 같은 함수형 오퍼레이터들로) 새로운 정보를 만들어 반환하는 것입니다. 그리고 이러한 “데이터의 흐름”이 함수형 프로그래밍의 원초이자 핵심이 됩니다.

    Elm the hard way의 예시를 보면,

    type alias Ship =
    { position : Float
    , velocity : Float
    , shooting : Bool
    }

    applyPhysics : Float -> Ship -> Ship
    applyPhysics dt ship = — dt(: Float)와 ship(: Ship)을 인자로 받는 함수.
    { ship | position = ship.position + ship.velocity * dt }
    다음의 함수와 같이, 평소와 같이 프로그래밍하면 됩니다. 가만, Elm의 모든 자료형은 불변아닌가? 내부적으로는, 함수의 어노테이션에서 유추할 수 있듯이 기존의 Ship 데이터를 받아서, 새로운 Ship 데이터를 리턴합니다. 한 마디로, 원래 인자로 준 Ship을 건들지 않고 새롭게 ‘또’ 만듭니다.

    Elm으로 작성되는 모든 애플리케이션은 필연적으로 Time Travel이라는 뒤로가기가 가능한데, 이러한 모든 자료(Ship)의 이력을 하나로 모은다고 생각하면, 무척이나 그것이 간단하다는 것을 알 수 있습니다.

    현대의 애플리케이션에도 이러한 모습을 볼 수 있는데, Reactjs의 Redux패턴은 여기에 영감을 받아, 아예 모든 상태를 하나의 트리(Store)에 때려박고, 상태를 변화시키려고 할 때마다 다시 그 이력을 추가하여 새로운 트리를 리턴합니다. (Redux의 모든 자료들 자체는 const 키워드를 쓰며, Action이라는 함수들을 따로 만들어 Store에게 상태변화를 요청하는 구조입니다) 이로써 SPA의 상태 관리를 하나의 전역 변수로 다룹니다. reduce 함수를 생각해보면 그 일을 하는 함수를 리듀서(reducer)라고 이름붙인 것은 우연이 아닙니다.
    Reducer(previousState, action) => newState;

    성능 상의 문제가 있지 않나? 라고 할 수도 있지만, React 자체가 증명했듯이, 영속적 불변 자료 구조 메커니즘, 즉 현명한 재계산(바뀐 부분만 재계산)을 통해 빠른 성능을 이뤄냅니다. 사실, Elm과 Reagent/Om(Clojurescript의 React 라이브러리)과 같은 불변형 언어들이 모두 React보다 빠릅니다.

    현명한 재계산 – (위의 새로운 Ship의 velocity와 shooting 상태는 모두 기존의 Ship의 메모리에 있는 상태를 같이 쓰면 되고, position만 변경되면 됩니다. 역설적으로 불변형이여서 그 상태들이 변하지 않음이 보장되니까, 마음대로 공유할 수 있습니다)

    사실 이러한 함수형의 진정한 진가는 단지 모든 것은 순수 함수이고, 단지 데이터(혹은 이벤트)를 보내면, 그 순수 함수들로 데이터를 이리저리 변형시키는 흐름. 궁극적으로 “모든 것이 투명한” 데이터의 흐름만이 남게 되는 프로그램의 작성입니다. 이렇게 되면, 통상의 프로그램 테스트라는 것이 그냥 입력/출력을 테스트하기만 하면 될 정도로 프로그램이 단순해집니다. Re-frame의 아키텍처를 보시면 도움이 될 것 같습니다.

    그렇게 되면 IO, 비동기 데이터 페칭, 브라우저 캐시, DB 참조등과 같은 사이드 이펙트만이 데이터의 투명한 흐름을 방해하는 마지막 요소가 되는데, 이러한 사이드 이펙트를 담당하는 구조를 만들어 따로 구분하는 게 보편적 방식인 것 같습니다.(Redux-saga, Cycle.js의 드라이버) 하스켈은 강력한 타입 시스템을 통해 모나드를 바탕으로, IO나 Maybe(에러), 비결정성 등 사이드이펙트의 여지가 있는 모든 것들을 보통(순수) 함수와 구분짓고 (그러나 마치 하나처럼 자연스럽게) 다룹니다.

    정리하자면, 그들이 말하는 불변성과 사이드 이펙트 없음은 우리가 처음 그 말을 들었을 때 이해되는 것과는 조금 다르다는 것입니다. 당연히 하스켈에서도 프로그램의 진행에 있어서 (우리가 보통 이해하는) 상태를 쓰고, 사이드 이펙트를 일으킵니다. 다만 (절대불변인것처럼) 상태가 투명해야 하고, (프로그램의 투명함에 지장을 주지 않도록 예민하게) 사이드 이펙트가 다루어져야 한다는 것입니다.

    저 역시 최근 Rust의 Trait과 타입 시스템에 흥미를 갖다가 하스켈에 푹 빠졌는데, 그동안 들어왔던 것들과는 달리 모나드를 통해 임의의 타입들에 특정 조건을 구현(또는 만족시켰다는 증명)시켜 임의의 Context로 승격(lift)시켜 통일적인 연산, 또는 취급을 한다는 생각뿐이 있을 뿐이지, IO를 쓰지 말자 이런 심오한(?) 배척은 없었습니다.

    “실제로 그런 것은 아니지만, 마치 그런 것처럼 다뤄보자.” 라고 한마디만 해줬어도 이런 오해는 없었을 것 같습니다.

댓글 남기기

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s