마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 1주차 요약.
프로그래밍 패러다임
과학의 경우 패러다임은, 어떤 학문 분야에서 구분되는 개념이나 사고 패턴을 설명한다.
프로그래밍 패러다임: 명령형(Imperative) 프로그래밍, 함수형(Functional) 프로그래밍, 로직(Logic) 프로그래밍
(객체 지향 프로그래밍은 이들 패러다임에 orthogonal하다)
명령형 프로그래밍과 한계
- 변경가능한 변수를 수정하고 할당을 사용하고 (메모리, 로드/저장 명령)
- 조건문, 루프, 브레이크, 리턴과 같은 제어 구조를 사용 (점프)
순수 명령형 프로그래밍은 명령어를 순차적으로 실행하는 구조로, 단어별로(word-by-word???) 데이터 구조의 개념을 도출하는 경향이 있다.
다항, 콜렉션, 문자열, 문서와 같은 고수준 추상화를 정의할 수 있는 다른 기법이 필요하다.
이론(Theory)이란?
이론은 다음으로 구성된다.
- 데이터 타입
- 타입에 대한 연산(오퍼레이션)
- 값과 연산 간의 관계를 기술하는 규칙
보통, 이론은 변경(mutation)에 대해 묘사하지 않는다.
변경 없는 이론 예1) 다항식 이론
(a*x + b) + (c*x + d) = (a+c)*x + (b+d) 계수를 변경하는 연산을 정의하지 않음
변경 없는 이론 예2) 문자열 이론의 문자열 연결 연산자
(a ++ b) ++ c = a ++ (b ++ c) 시퀀스 원소 변경 연산자를 정의하지 않음
프로그래밍을 위한 결론
- 연산자를 위한 이론을 정의하는데 집중
- 상태 변경을 최소화
- 연산자를 함수로 여기고, 종종 단순한 함수의 조합으로 처리
함수형 프로그래밍과 언어
- |
함수형 프로그래밍 |
함수형 프로그래밍 언어 |
좁은(restricted) 의미 |
변경가능한 변수, 할당, 루프, 다른 명령형 제어 구조 없는 프로그래밍 |
변경 가능 변수, 할당, 명령형 제어 구조가 없는 언어 |
넓은(wider) 의미 | 함수에 집중하는 것 | 함수를 이용해서 elegant한 프로그램 을 구성하는 것 |
함수형 프로그래밍 언어에서 함수는 일급(first-class)이다.
- 모든 곳에 정의 가능함 (다른 함수 내부 포함)
- 다른 값처럼 함수를 파라미터로 전달 가능하고 결과로 리턴될 수 있음
- 함수를 조합하는 집합 연산자 존재
요즘 함수형이 뜨는 이유
멅티코어와 클라우드 컴퓨티 환경에서 병렬을 활용하는 좋은 방법을 제공하기 때문이다.
프로그래밍의 요소(Elements of Programming)
언어는 다음을 제공한다.
- 단순한 요소를 표현하는 프리미티브(원시) 식(primitive expression)
- 식을 묶는 방법
- 식을 추상화하는 방법(식에 이름을 붙이고 이를으로 참조)
식의 계산(evaluation)
원시 식이 아닌 연산자를 포함한 식은 다음 과정을 따라 계산한다.
- 가장 왼쪽 연산자를 선택
- 선택한 연산자의 피연산자의 값을 구함(왼쪽 피연산자의 값을 먼저 구함)
- 두 피연산자에 연산자를 적용
- 모든 함수 인자를 왼쪽부터 오른쪽으로 차례대로 계산
- 함수 적용을 함수의 우측(함수의 몸체)으로 대치하고, 동시에
- 함수의 formal 파라미터를 실제 인자로 대치함
값 계산의 두 가지 전략
- call-by-value(이하 CBV): 식을 값으로 바로 계산해서 전달, 모든 함수 인자를 한 번만 계산하는 장점.
- call-by-name(이하 CBN): 식의 값이 실제로 필요할 때까지 이름으로 전달, 함수에서 사용하지 않는 인자는 함수 몸체를 적용하는 과정에서 계산하지 않는 장점
두 방식은 다음 조건을 충족하는 한 두 방식은 동일한 값으로 reduce된다.
- reduce한 식이 순수 함수로 구성
- 둘 다 계산이 끝남
CBV 식이 끝나면 동일한 CBN 식도 끝나지만, 반대는 성립하지 않는다. 아래 코드에서 CBN의 경우 first(1, loop)는 1로 계산되지만 CBV의 경우 두 번째 인자인 loop의 값 계산이 끝나지 않아 식이 끝나지 않는다.
def first(x: Int, y: Int) = x
first(1, loop)
조건 식
스칼라의 if-else은 식이다. if의 predicate은 Boolean 값이다. &&와 ||는 왼쪽 피연산자의 값에 따라 오른쪽 피연산자를 계산하지 않는다. 예를 들어, true || e의 경우 왼쪽이 true이므로 오른쪽의 e 값을 구할 필요가 없으므로 결과는 true가 된다. 이런 식을 "short-circuit evaluation"을 사용한다고 말한다.
중첩 함수
스칼라는 함수 안에서만 접근할 수 있는 중첩 함수를 정의할 수 있다. 특정 함수 안에서만 사용하는 함수를 중첩함으로써 "name-space pollution"을 피할 수 있다.
블록
스칼라의 블록은 중괄호로 구분하며, 정의나 식을 포함한다. 블록 그 자체는 식이므로 식이 올 수 있는 모든 자리에 블록이 올 수 있다.
블록 안에 위치한 정의는 블록 안에서만 보이며, 블록 밖의 같은 이름을 가진 정의를 감춘다. 블록 밖의 정의는 블록 안에서 접근할 수 있다.
val x = 0
val y = 1
def f(y: Int) y + 1
val result = {
val x = f(3) // 블록 밖의 x를 감춤
x * x + y // 블록 안의 x임, 블록 밖의 정의에 접근 가능
} + x // 블록 밖의 x
꼬리 재귀(tail recursion)
함수의 마지막 액션이 자신을 호출하면, 함수의 스택 프레임을 재사용할 수 있다. 이를 꼬리 재귀라 한다. 스칼라는 꼬리 재귀를 최적화한다.
def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) // 마지막 액션이 꼬리 재귀, 최적화함
def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n-1) // 마지막 액션이 꼬리 재귀 아님