도커(Docker)로 리스프 실행하기
리스프로 함수 정의하기
함수형 언어에서 가장 중요한 건 역시 함수다. 리스프에서 함수를 정의할 때는 defun이라는 키워드를 사용한다.
(defun 함수_이름
(인자1 인자2...)
"주석"
함수_정의)
함수를 정의할 때도 괄호로 시작하고 괄호로 끝난다. 리스프에서는 예외 없이 모든 것이 괄호로 시작하고 괄호로 끝난다고 생각하면 된다. 그럼 두 수의 합을 구하는 sum 함수를 작성해 보자.
(defun sum (x y) "sum x and y" (+ x y))
(sum 1 5)
// 6
여기서 짚고 넘어가야 할 점이 두 가지 있다.
- 함수를 정의할 때 인자의 타입을 지정하지 않는다는 점(동적 타이핑 언어)
- 리스프에서 함수는 반드시 하나의 값을 반환한다.
절차 지향적인 언어에서의 함수는 그저 여러 줄의 코드를 묶어서 함수로 정의해 놓고, 필요할 때 호출하면 해당 코드가 수행되는 것이 절차 지향 언어에서의 함수다. 거기에도 입력과 출력의 개념이 있지만, 어디까지나 필수적인 요소가 아닌 선택적인 요소일 뿐이다.
반면 함수형 언어에서의 함수는 다른 말로 표현식이라도 한다. 즉, 식인 것이다. 수학에서도 식에 입력값을 적용하면 식의 모양이 바뀐다. 마찬가지로 함수형 언어에서도 함수에 입력값을 적용하면 식이 바뀌다가 결국 최종 값을 반환하게 된다.
리스프에서의 변수(Variable)
전역 변수에 값을 설정하기 위한
- defvar
- defparameter
지역 변수를 정의하기 위해 사용하는
- let
- let*
defvar은 한번 값을 할당하면 값에 대한 재할당이 무시된다.
>>(defvar *x* 123)
*x*
>> *x*
123
>>(defvar *x* 555)
*x*
>> *x*
123
x에 123이란 값을 설정한 후 555란 값을 재설정했지만 무시된 것을 알 수 있다. 반면, defparameter는 할당된 값을 자유롭게 바꿀 수 있다.
>> (defparameter *y* 123)
>> (defparameter *y* 555)
>> *y*
555
>> (+ 5 *y*)
560
전역 변수를 정의하면 어디에서도 사용할 수 있다.
let과 let*에 대해서 알아보자
>> (let ((x 1)) (+ x 1))
2
x라는 지역 변수에 1이란 값을 대입했고, 그 밑의 식에서 x라는 변수를 사용했다. 한편, let 문을 벗어난 곳에서는 x를 사용할 수 없다.
*** - SYSTEM::READ-EVAL-PRINT: variable X has no value
2개 이상의 지역 변수를 선언하는 예는 다음과 같다.
>> (let ((x 1) (y 2)) (+ x y))
3
한편, let*를 사용하면 지역 변수에 값을 할당 시 다른 지역 변수 값을 상호 참조할 수 있다.
>> (let* ((x 1)(y x)) (print y))
1
리스프에서의 조건 분기(if)
(if (조건식)
(true일 때)
(false일 때))
전역 변수를 정의하고 그 값에 대한 조건식으로 분기하는 코드는 다음과 같다.
>> (defvar x 123)
123
>> (if (> x 3) "yes!!" "no!!")
yes!!
이번에는 함수 내에서 if 분기를 사용해 보자.
>> (defun compare3 (x) (if (> x 3) "bigger" "smaller"))
COMPARE3
>> (compare3 5)
"bigger"
>> (compare3 2)
"smaller"
if 표현식이 참일 때, 2개 이상의 표현식을 지정하고 싶은 경우는 어떻게 해야 할까? 이때는 progn이란 키워드를 사용하여 2개의 표현식을 하나의 표현식으로 묶으면 된다.
>> (defvar x 123)
123
>> (if (> x 3) (progn (print "yes") (print "and this too!")) (print "no"))
"yes"
"and this too!"
조금 복잡한 조건 분기를 기술하다 보면 다음과 같이 if 표현식이 중첩된다.
(if (> x 0)
(if (> x 10)
"over 10"
"between 0 and 10")
"minus")
if문의 중첩되면 가독성이 떨어지기 때문에 cond라는 키워드를 사용하는 것이 좋다.
>> (cond ((< x 0) "minus") ((> x 10) "over 10") (t "else"))
"over 10"
다른 프로그래밍 언어에서의 switch문과 비슷하다. 한 가지 주의해야 할 점은 위에서부터 차례대로 true 여부를 확인하므로 조건식을 나열하는 순서가 중요하다는 점이다. 또한, 관례적으로 조건식의 마지막에는 else에 해당하는 평가식을 기재한다.
리스프에서의 리스트
리스트(LISP)라는 이름은 LISt Processing의 약자다. 리스트 처리가 리스프의 핵심 목표였음이 분명하다. 리스트는 복수의 데이터가 줄줄이 연결된 자료 구조다. 매우 단순한 구조이지만 컴퓨터 공학에서 차지하는 비중이 매우 높으며, 특히 함수형 언어를 이해하고 연습하는데 중요한 자료구조다.
리스트 표현식
>> '(1 2 3 4 5)
괄호 안에 데이터를 넣고 그 앞에 '(쿼트)를 넣어 주면 리스트 데이터로 인식된다. REPL에서 위 식을 입력하면 다음 내용이 출력된다.
>> '(1 2 3 4 5)
(1 2 3 4 5)
한편, 지금까지 사용한 S-표현식의 일반적인 형태는 다음과 같다.
(함수_이름 인자1 인자2 ...)
가만 보니 S-표현식 자체가 리스트다. 리스프의 코드 자체가 리스트인 것이다.
이처럼 코드와 데이터가 같은 방식으로 기술되고 처리되는 특징은 code as data 혹은 동형성(Homoiconicity)이라고 표현한다.
cons와 append 함수
리스트에 값을 추가할 때는 크게 const와 append 함수를 사용한다.
>> (cons 1 '(2 3))
(1, 2, 3)
한편, append 함수는 리스트와 리스트를 합쳐서 하나의 리스트로 만들 때 사용한다.
>> (append '(1 2) '(3 4))
(1 2 3 4)
cons로 리스트와 리스트를 하나로 합치면 다음과 같이 중첩된 리스트로 합쳐진다.
>> (cons '(1 2) '(3 4))
((1 2) 3 4)
리스트와 재귀 함수
일반적인 절차 지향 언어에서는 리스트를 처리할 때 for문을 많이 사용한다.
list = [1, 2, 3, 4, 5]
for (int i = 0; i < list.length; i++)
{
print(list[i])
}
이러한 for문은 i라는 변수의 값을 바꿔가면서 수행된다는 측면에서 함수형 언어의 원리에 어긋난다. 함수형 언어에서는 반복문을 재귀 함수로 구현할 수 있다. 먼저, 재귀 함수의 정의에 대해서 알아보자.
재귀 함수란, 함수의 정의 내에서 자기 자신을 호출하는 함수를 말한다. 다음은 그 예시다.
(defun myself (x) (if (> x 10) "finish" (progn (print x) (myself (+ 1 x)))))
(myself 0)
0
1
2
3
4
5
6
7
8
9
10
"finish"
재귀 함수의 조건 세 가지를 확인할 수 있다.
- 재귀 함수는 자기 자신을 호출해야 한다.
- 재귀 함수는 종료 조건이 있어야 한다.
- 재귀 함수가 종료 조건에 수렴하기 위해서는 반드시 자기 자신에게 전달된 입력값과 다른 값을 사용해서 자기 자신을 호출해야 한다. 함수형 프로그래밍의 원리에 따르면 함수 내에서는 외부 변수에 접근하지 않는 것이 좋다. (순수 함수, 부작용) 따라서 재귀 함수의 종료 조건은 입력 인자를 참조하지 않을 수가 없다.
피보나치 수 구하기
피보나치 수(Fibonacci numbers)는 첫째 및 둘째 항이 1이고, 이어지는 숫자는 바로 앞 두 항의 합인 수열이다. 따라서 세 번째 항은 1 + 1 = 2이고, 네 번째 항은 1 + 2 = 3이 된다. 값을 일부 나열해 보면
1, 1, 2, 3, 5, 8, 13 ...
(defun fibo (n) (cond ((= n 1) 1) ((= n 2) 1) (t (+ (fibo (- n 1)) (fibo (- n 2))))))
>> (fibo 10)
55
피보나치 수열을 통해 재귀 함수의 본질에 대해 한 가지 더 고찰해 본다면 다음과 같은 특징을 발견할 수 있다. 바로, 재귀 함수가 성립하기 위해서는 재귀 함수의 입력 인자들과 그 출력값들 사이에 일정한 관계가 있어야 한다는 점이다.
함께 읽으면 좋은 글
'프로그래밍 > 함수형 프로그래밍' 카테고리의 다른 글
스칼라(Scala) 기본 문법에 대해서 알아보자 (3) | 2024.07.22 |
---|---|
리스프(LISP) 재귀 함수 구현해보자 (4) | 2024.07.16 |
C#의 커링(Curring) (0) | 2024.03.25 |
일급 함수(First-class function) vs 고차 함수(Higher-order function) (0) | 2024.03.25 |
함수형 프로그래밍 관련 개념 정리 (0) | 2024.03.22 |
댓글