수학적 컴퓨팅을 위한 함수형 프로그래밍
프로그래밍 언어는 문제에 대한 일반적인 해법을 기술하도록 도와주며, 그 결과물이 우연히 기계에서 실행 가능한 형태가 됩니다. 모든 프로그래밍 언어는 저마다 다른 강점과 약점을 가지고 있는데, 그 이유 중 하나는 언어의 문법과 의미론이 쉽게 다룰 수 있는 문제의 범위에 큰 영향을 미치기 때문입니다.
tl;dr: 저는 더 흔한 명령형 접근 방식보다 함수형 프로그래밍이 수학적 계산에 더 적합하다고 생각합니다.
수학을 위한 내장 추상화 활용하기
언어 뒤에 있는 아이디어(기저에 깔린 프로그래밍 패러다임)는 그 언어를 중심으로 형성되는 커뮤니티를 특징짓습니다. 개발자들은 언어 코어를 중심으로 바로 사용할 수 있는 라이브러리와 프레임워크로 이루어진 독자적인 생태계를 만들어 냅니다. 그 결과 어떤 언어는 비즈니스 애플리케이션 같은 분야에서 더 강하고(Cobol을 떠올릴 수 있습니다), 어떤 언어는 C나 Rust처럼 시스템 프로그래밍에 탁월합니다.
컴퓨터로 수학적·수치적 문제를 해결할 때를 이야기한다면 Fortran이 떠오를 수 있습니다. Fortran은 범용 언어이지만 주로 과학 계산 분야에서 알려져 있습니다. 물론 이 언어는 애초에 그런 목적으로 만들어졌습니다. 그래서 이름도 Formula Translation입니다.
이 분야에서 인기가 높은 이유 중 하나는 성능을 고려하면서도 수학적 개념을 표현할 수 있는 도메인 특화 키워드를 내장하고 있기 때문입니다. 예를 들어 복소수를 위한 전용 데이터 타입인 COMPLEX와, 수학적 용어와 매우 유사하며 배열과 벡터를 만드는 데 사용할 수 있는 DIMENSION이라는 키워드를 제공합니다.
명령형 스타일 vs 함수형 스타일
내장 키워드는 특정 문제 영역에서 언어의 표현력을 확장하는 데 도움이 될 수 있지만, 이 접근법은 심각하게 제한적입니다. 언어 코어를 ad infinitum으로 무한히 확장하는 것은 현실적이지 않습니다. 그렇게 하면 유지보수가 더 어려워지고 배우는 데 더 오랜 시간이 걸릴 뿐입니다. 따라서 대부분의 언어는 루틴을 더 작고 관리하기 쉬운 부분으로 나누기 위해 functions, subroutines, classes와 objects 같은 다른 추상화 수단을 제공합니다. 이러한 메커니즘은 프로그램의 복잡도를 제어하는 데 도움이 될 수 있지만, 특히 수학적 문제를 다룰 때는 상용구 코드로 해법을 흐리지 않도록 주의해야 합니다.
사례 I - 팩토리얼
예를 들어, 양수 n의 팩토리얼을 계산하는 다음 공식을 프로그램 코드로 옮기는 것이 과제라고 해 보겠습니다.
위 공식을 명령형 스타일의 Java로 구현하면 다음과 같이 보일 수 있습니다.
public static long fact(final int n) {
if (n < 0) {
// Negative numbers not allowed
return 0;
}
long prod = 1;
for (int i = 1; i <= n; ++i) {
prod *= i;
}
return prod;
}이렇게 짧은 문제 정의에 비하면 상당히 긴 해법입니다. (1부터 n까지 명시적인 루프를 사용한 버전을 의도적으로 작성했다는 점을 참고해 주십시오. 재귀 함수를 사용하면 더 짧아지겠지만, 수학적 공식에는 없던 개념을 사용하게 됩니다.)
또한 이 프로그램은 public, static, System.err.println() 같은 언어 고유의 키워드를 많이 포함합니다. 게다가 프로그래머는 사용하는 모든 변수에 대해 데이터 타입을 명시적으로 제공해야 합니다. 번거로운 의무입니다.
이 모든 것이 수학적 정의를 흐리게 만듭니다.
이를 Haskell 같은 함수형 언어로 작성된 다음 버전과 비교해 보십시오.
fact n = product [1..n]이는 문제 정의를 코드로 거의 직접 옮긴 것입니다. 명시적인 타입도, 임시 변수도, 접근 제한자(예를 들어 public)도 필요하지 않습니다.
사례 II - 내적
위의 Haskell 프로그램이 짧은 이유는 해당 작업에 꼭 맞는 추상화(바로 product 키워드와 [1..n] 범위 문법)를 언어가 제공하기 때문이라고 주장할 수도 있습니다. 따라서 Haskell에도 Java에도 없는 간단한 함수, 즉 두 벡터의 내적을 살펴보겠습니다. 수학적 정의는 다음과 같습니다.
3차원 벡터의 경우 다음과 같이 쓸 수 있습니다.
먼저 Haskell 구현입니다.
type Scalar a = a
data Vector a = Vector a a a deriving (Show)
dot :: (Num a) => Vector a -> Vector a -> Scalar a
(Vector a1 a2 a3) `dot` (Vector b1 b2 b3) = a1*b1 + a2*b2 + a3*b3수학적 타입을 각각 한 줄로 정의할 수 있다는 점에 주목해 주십시오. 또한 dot 함수를 중위 표기법으로 정의한다는 점에도 주목해 주십시오. 즉, dot의 첫 번째 인자를 함수 이름 앞에, 두 번째 인자를 뒤에 둡니다. 이렇게 하면 코드가 수학적 표현과 더 비슷해 보입니다. 위 함수의 호출 예는 다음과 같습니다.
(Vector 1 2 3) ’dot’ (Vector 3 2 1)짧고 정확하며 읽기 쉽습니다.
이제 Java로 유사하게 구현한 버전입니다.
public static class Vector<T extends Number> {
private T x, y, z;
public Vector(T x, T y, T z) {
this.x = x;
this.y = y;
this.z = z;
}
public double dot(Vector<?> v) {
return (x.doubleValue() * v.x.doubleValue() +
y.doubleValue() * v.y.doubleValue() +
z.doubleValue() * v.z.doubleValue());
}
}
public static void main(String[] args) {
Vector<Integer> a = new Vector<Integer>(3, 2, 1);
Vector<Integer> b = new Vector<Integer>(1, 2, 3);
System.out.println(a.dot(b));
}
}Vector를 제대로 된 텍스트 형태로 표현하려면 toString() 메서드도 오버라이드해야 합니다. Haskell에서는 코드에서 보듯이 Show 타입클래스를 단순히 derive하면 됩니다.
새로운 추상화 만들기
함수와 타입만으로 직관적인 프로그램을 작성하기에 충분하지 않다면, Haskell은 언어 코어 자체를 확장하는 새로운 연산자와 키워드를 만들 수 있는 간단한 구문도 제공합니다. 이를 통해 도메인 특화 언어를 만들 수 있으며, 개발자가 프로그래밍 언어 자체의 특수성(예를 들어 메모리 관리나 배열 순회)을 우회하는 대신 실제 문제에 더 직접적으로 집중할 수 있게 됩니다. Haskell은 이 개념을 적극적으로 받아들입니다. Java에는 이러한 기능이 없습니다.
결론
여기서 Java를 깎아내리거나 Haskell을 찬양하려는 것은 아닙니다. 두 언어 모두 각자의 자리가 있습니다. 단지 많은 프로그래머가 읽을 수 있기 때문에 Java를 예로 든 것뿐입니다.
이 비교는 수치 및 기호 프로그래밍을 위한 함수형 접근 방식과 명령형 접근 방식 사이의 비교에 가깝습니다. 그리고 그 영역에서는 저는 언제나 함수형 접근 방식을 선호합니다. 불필요한 요소를 제거하고 우아한 해법을 만들어 내기 때문입니다. 높은 추상화 수준에서 작업하고 수학적 언어로 말할 수 있는 편리한 방법을 제공하는데도, 많은 프로그래머가 이러한 강점을 간과하고 있습니다.
1966년 저서 The Psychology of Science에 나온 에이브러햄 H. 매슬로(Abraham H. Maslow)의 관찰이 적절해 보입니다.
“가지고 있는 유일한 도구가 망치라면, 모든 것을 못인 양 다루고 싶은 유혹을 느끼기 마련이다.”
글을 무작위로 읽기