Stop Thinking, Just Do!

Sungsoo Kim's Blog

Advantages of Functional Programming

tagsTags

11 September 2015


Advantages of Functional Programming

요즘 스칼라를 통해 함수형 프로그래밍(Functional Programming)을 공부하고 있는데, 문득 함수형 프로그래밍의 장점을 요약해 표현하라면 뭐라고 설명해야 할지 생각해보게 됐다. 우선, 객체 지향 언어와 함수형 언어의 가장 근본적인 차이점은, 객체지향 언어의 first-class citizen은 클래스 내지 객체가 되는 반면에 함수형 언어는 함수 그 자체가 first-class citizen이 된다는 점이다.

First-Class Citizen

First-class citizen이란 프로그래밍 언어의 설계 시에 런타임에 프로그램 흐름의 중심으로 결정한 엔터티를 의미한다. 예를 들어, 객체 지향 언어에서 first-class citizen은 ‘객체’라 볼 수 있으며, 프로그램이 실행되면 객체들 간의 관계(어떤 객체가 어떤 객체를 참조하는지, 메소드는 어떤 객체를 인수로 받아서 어떤 객체를 반환하는지)에 의해 프로그램의 행태가 결정된다. 위키피디아의 설명에선 first-class citizen 엔터티(객체)의 조건을 다음과 같이 정리하고 있다.

  • 변수와 자료구조에 저장될 수 있다.
  • 하위 루틴의 파라미터로써 전달될 수 있다.
  • 하위 루틴의 수행 결과로써 반환될 수 있다.
  • 런타임에 구성될 수 있다.
  • 어떤 명칭(예, 변수명)이 부여되는지에 상관 없이, 독립적이고 고유한 엔터티다.

즉, 함수형 언어에선 함수 자체가 객체 지향 언어의 클래스와 같은 일종의 타입으로 동작한다. 함수는 일종의 ‘값’으로써 다른 함수의 인자로 전달될 수 있다. 프로그램은 주어진 문장(statement)을 수행하기 보다, 주어진 표현식(expression)을 계산(evaluate)하는 방식으로 구성된다.

그럼, 좀 더 구체적으로 ‘함수’를 중심으로 구축되는 함수형 언어의 특징을 정리하기 위해 함수형 언어 중 하나인 Haskell의 웹 페이지에서 설명하고 있는 일반적 특징을 살펴보도록 하자. Haskell은 함수형 언어 중 하나로, 스칼라보다 더 엄격하게 함수형 프로그래밍 패러타임을 따르고 있는 언어다.

1. Higher-Order Functions (HOFs)

앞서 언급했듯이, 함수형 언어에선 함수가 다른 함수의 파라미터로 사용될 수 있다. HOF란 다른 함수를 자신의 파라미터로 취하는 함수를 지칭한다. 단순히 함수로 값(또는 참조)을 전달하는 기존 관념을 넘어, 함수라는 제어 흐름 자체를 파라미터로써 수용함에 따라, HOF는 객체 지향적 관점에선 찾을 수 없는 근본적 이점을 도입할 수 있다. CMU의 “Programming in Standard ML”에서 설명하고 있는 HOF의 주요 쓰임(특징) 두 가지는 다음과 같다.

  • 제어 패턴의 추상화 (Abstracting Patterns of Control): HOF는 계산의 세부 사항을 별도의 함수 안에 캡슐화 해서 추상적으로 제공(‘abstract out’)할 수 있도록 한다. 예를 들어, 함수형 언어에선 객체 지향 언어에서 일상적으로 사용하는 반복문(for, while)을 개별 함수로 일반화 및 추상화 해서 반복문 자체를 대체할 수 있는 경우가 많다. 결국, 이는 프로그래머의 생산성과 코드 품질을 높은 수준으로 가져갈 수 있는 원인이 된다.
  • 계산 단계의 조율 (Staging Computation): 계산에 투입되는 파라미터는 초기에 확정되는 인수(early argument)와 계산의 과정에서 차차 결정되는 인수(late argument)로 나눠 볼 수 있다. 만약 초기에 확정되는 인수가 있다면 최대한 미리 확정하고, 계산이 진행 시에는 나중에 결정되는 인수에 해당하는 함수 만을 처리토록 하면 프로그램의 효율을 높일 수 있다. HOF는 초기에 확정되는 인수를 미리 밝혀내는 단계와 나중에 결정되는 인수를 처리하는 단계를 별도로 분리해, 계산을 단순하고 효율적으로 처리하는 도구로 쓰일 수 있다.

2. Purity

‘함수’라는 일반적 단어의 의미에서도 알 수 있듯이, 함수형 언어에서 함수의 역할은 계산(evaluate)된 결과를 도출하는 일이다. 프로그래밍의 편의를 위해 함수형 언어의 함수 내부에서도 계산 결과와는 상관 없는 별도의 동작(statement에 해당)을 수행할 수는 있지만, 이는 함수형 프로그래밍의 개념 상에서 볼 땐 함수의 본래 역할에서 벗어나 함수의 I/O와 상관 없이 시스템의 상태를 관리/조작하는 부작용(side-effect, 자세한 설명은 이 블로그 기사를 참고해보자)를 발생시키는 원인이다. 물론, 이런 부작용을 완전히 제거하긴 어렵겠지만, 최대한 줄이려는 노력을 기울일 수록(purify) 시스템은 보다 직관적이고 강경한 구조(correctness를 더 높게 보장할 수 있는 구조)로 구축될 수 있다. 이런 측면에서, 함수형 언어는 함수적 순수성을 최대한 유지하기 위해 다음과 같은 속성을 지향한다.

  • 수정할 수 없는 데이터 (Immutable Data): 데이터의 수정이 필요할 땐 데이터의 값을 수정하기 보단 새로운 복사본을 만들며 값의 변경을 반영한다. 이렇게 만들어진 복사본 역시 수정할 수 없다. 이를 통해 안전한 데이터 참조가 가능해진다.
  • 관계 투명성 (Referential Transparency): 순수한 계산(pure computation)은 언제나 같은 값을 반환한다. 이를 ‘관계 투명성’이라 한다. 즉, y = f(x)라는 함수를 수행한 후에 g = (h(y))(y)에 입력한 g의 결과와, g = (h(f(x)))f(x)와 같이 g를 바로 수행하며 각각의 y에 대해 f(x)를 별도로 수행해 입력한 결과가 동일해야 한다.
  • 지연 계산 (Lazy Evaluation): 관계 투명성은 함수가 언제 수행되든 동일한 결과가 도출됨을 보장한다. 따라서, 불필요한 계산을 방지하기 위해 실제로 해당 계산이 필요할 때 까지 계산의 수행을 미룰 수 있다.

물론, 이런 순수성을 무조건 지키도록 강제해선 프로그래밍 언어의 중요 목적 중 하나인 프로그래머의 생산성에서 오히려 문제가 발생할 수도 있다. 때문에 실제 함수형 언어에선 순수성 지향하면서도 부작용을 유발하는 요소를 언어의 기능에 함께 포함시킨 경우가 많다.

3. Recursion

함수형 프로그래밍에선 재귀 호출이 빈번하게 사용된다. 때론 재귀 호출이 반복문을 대체할 수 있는 유일한 방법인 경우도 있으며, 주어진 함수를 사용해 원하는 값을 계산할 때 까지의 프로그램 흐름을 만들어가는 필수 장치다. 재귀 호출의 빈번한 사용에 따른 비용을 최소화하기 위해, 함수형 언어는 Tail Call(Tail Recursion) 기능을 통해 콜-스택의 불필요한 생성을 방지하며 메모리 낭비를 제거하는 등의 최적화 방안을 제공한다.

함수형 프로그래밍은 연속된 명령문(statement)을 통해 프로그램의 상태를 변경하는 데만 의존하는 순수한 명령적 프로그래밍(imperative programming) 패러다임과 정 반대의 위치에 있다. 반면에, 하위 루틴과 블록 구조, 반복문 등을 활용해 프로그램 구조를 구축하는 구조적 프로그래밍(structured programming) 패러다임에는 완전히 부합한다. 순수한 명령적 프로그래밍에선 명령문에 의해 변경되는 상태가 시스템 전체로 공유되며, 이는 상태 관리의 어려움을 초래한다. 즉, 명령적 프로그래밍은 상태 공유에 다른 부작용이 극대화되는 문제가 있으며, 따라서 구조적 구성을 통해 상태가 공유되는 범위를 제한(함수나 객체와 같이)하는 노력이 계속됐다. 객체 지향의 패러다임은 클래스라는 타입에 맞춰 공유 상태를 분류하고, 객체라는 경계에 따라 공유 범위를 제한한다. 비록 객체의 상태는 시스템에 공유될 수 있지만, 일부 상태는 객체 외부로부터 숨길 수 있으며(information hiding), 모듈화 된 그릇에 상태를 모아 관리하기 때문에 순수한 명령적 프로그래밍에 비해 상당히 구조적으로 변화한 형태다. 함수형 프로그래밍은 여기서 더 나아가 공유 상태를 함수 범위(객체 범위보다 좁게)로 제한하는 방향을 지향한다(앞서 살펴 본 purity의 개념과 같은 맥락). 결국 순수한 함수형 언어에선 상태가 함수 밖으로 드러나지 않으며, 함수는 I/O만을 제공하고 상태의 측면에선 완전한 블랙박스가 된다. 따라서 프로그래밍의 구조화 정도는 객체 지향적 개념보다 함수형 프로그래밍이 더 높아 보인다(Haskell과 같은 순수 함수형 언어는 앞서 언급한 ‘부작용’에서 상당히 자유롭다).

다시 정리해보자. 프로그래밍 언어는 명령적(imperative) 방식에서 구조적(structured) 방식에 가까워짐에 따라 시스템 상태의 관리가 정확해지고 상태 조작의 부작용이 줄어들게 된다. 프로그램은 추상화를 통해 컴포넌트를 구축하고, 컴포넌트 간에 인터페이싱이 가능토록 함으로써 구조화할 수 있다. 객체 지향 언어(스칼라와 같은 함수형 언어도 사실 객체 지향의 개념을 담고 있는데, 여기서의 논의하는 객체 지향이란 함수형으로써의 특징이 부족한 ‘자바’ 언어 정도로 생각하자)도 이런 구조화에 대한 고민의 결과로 볼 수 있지만, 함수형 언어는 객체 지향 언어보다도 더 강력한 추상화를 제공하기 때문에 좀 더 구조적일 수 있다. 즉, 함수형 언어는 객체 지향 언어보다 깨끗하고 깔끔한 추상화 방법을 제공하며, 따라서 상태 변경에 따른 부작용에서 더 자유롭다. 이는 함수적으로 작성된 코드가 다른 코드(예, 객체 지향의 코드)에 비해 더욱 선언적(declarative)이고 포괄적(comprehensible)임을 의미한다. 여기에 함수형 프로그래밍의 근본적 장점이 있다.


comments powered by Disqus