최근 수정 시각 : 2024-10-14 14:16:00

Scheme/튜토리얼


파일:상위 문서 아이콘.svg   상위 문서: Scheme
1. 개요2. 기본3. 타입
3.1. 불린, 글자, 문자열3.2. 숫자3.3. 리스트와 벡터3.4. 심볼3.5. 프로시저
4. 기본 폼
4.1. lambda4.2. set!4.3. if
5. Derived 폼
5.1. let, let*, letrec5.2. begin5.3. and5.4. or5.5. cond5.6. case
6. 외부 링크

1. 개요

이 문서는 Try Scheme에서 제공하는 튜토리얼 문서를 정리한 것이다. 원본 문서는 아래와 같이 두 개의 라이선스를 채택했다.
  • Apache 2.0
  • LGPL

2. 기본

  • 자세한 내용은 R7RS를 참고하라.
  • REPL에서 전통적인 헬로 월드 프로그램을 실행하려면 아래와 같이 display 프로시저procedure를 사용한다.(스킴에서는 함수function보다 프로시저라는 용어를 선호한다.)
    {{{> (display "hello world!\n")
hello world!
}}}
  • 프로시저는 아래와 같이 괄호 문법을 사용한다.
    [math((<프로시저>\ <인자1>\ <인자2> ...))]
  • 더하기나 곱하기 같은 간단한 산술 연산도 프로시저 호출로 한다.
    {{{> (+ 6 1)
7
(expt 2 8)
256
(* 4 (atan 1))
3.141592653589793
}}}
  • defineform은 전역 변수와 전역 프로시저를 정의할 때 사용한다.
    {{{> (define pi (* 4 (atan 1)))
(define circum (lambda (r) (* 2 pi r)))
(circum 5)
31.41592653589793
}}}
  • 위 코드는 전역 변수 picircum을 정의한다. circum 변수에 저장된 것은 프로시저이다. 이 프로시저는 지역 변수 r 하나를 받고 lambda 폼으로 만든 것이다. 이 프로시저 정의는 변형된 define 폼으로 더 간단하게 만들 수 있다.
    {{{(define (circum r) (* 2 pi r))
}}}
  • 어떤 변형으로 정의하더라도 define 폼으로 입력한 것은 REPL에서 결과를 출력하지 않는다.
  • 라이브러리가 제공하는 기능을 사용하려면 import 폼을 사용한다. 예를 들어 SRFI 48 라이브러리에 format 프로시저를 이용하면 출력 포맷을 다룰 수 있다.
    {{{> (import (srfi 48))
(format #t "1+2=~s\n" (+ 1 2))
1+2=3
}}}
  • 일반적인 스킴 프로그램은 import 폼, 정의, 프로시저 호출의 연속이다. 소스 코드 확장자는 .scm이나 .sld를 사용한다. 파일에 저장된 스킴 코드를 REPL에서 실행하려면 load 프로시저를 사용한다.
    {{{(import (srfi 19))
(import (srfi 48))

(define (get-date)
(time-utc->date (current-time time-utc)))

(format #t
"current UTC time: ~a\n"
(date->string (get-date)))
}}}
  • 위 예제 나온 ->처럼 !, ?, =, <, +, / 등 글자가 아닌 문자도 스킴 식별자로 사용할 수 있다. 한 줄 주석은 ; 두 개로 시작한다.

3. 타입

3.1. 불린, 글자, 문자열

  • 불린, 문자, 문자열 문법은 아래와 같다.
    • #f: 거짓
    • #t: 참
    • #\X: 문자 X
    • #\space: 공백 문자
    • "ABC\n": 네 글자 문자열
  • 이 타입을 다루는 몇 가지 연산은 아래와 같다.
    {{{> (string-append "A" "BC")
"ABC"
(string-ref "XYZ" 2) ;; 인덱싱
#\Z
(string-length "AB") ;; 길이 구하기
2
(char->integer #\A) ;; 변환
65
(char<? #\A #\Z) ;; 비교
#t
}}}
  • 스킴 문자열은 수정할 수 있다. string-set! 프로시저로 문자를 수정할 수 있다.

3.2. 숫자

  • 스킴에는 정수, 실수, 복소수를 포함한 완전한 수치 타입들이 있으며, 예를 들어 42, -3.5, 1+2i가 있다. 또한 숫자는 해당 값이 수학적으로 정확한 값(정확)인지 또는 근삿값(부정확)으로 취급되어야 하는지에 대한 표시를 가진다. 예를 들어 42.0은 부정확한 정수이고 2/3은 정확한 실수이다. 정확한 숫자는 무한한 크기를 가질 수 있다. 다음은 다양한 타입의 숫자에 대한 몇 가지 연산이다.
    {{{> (expt 2 75) ;; 2^75
37778931862957161709568
(exp (* 75 (log 2))) ;; e^(75*ln(2))
3.7778931862957074e22
(* (/ 5 3) (/ 9 2)) ;; 5/3 * 9/2
15/2
(+ (sqrt -6.25) 1) ;; sqrt(-6.25)+1
1+2.5i
(> 1e-2 1) ;; 0.01 > 1 ?
#f
}}}

3.3. 리스트와 벡터

  • 임의의 값들로 이루어진 유한한 시퀀스를 나타내기 위한 두 가지 타입이 있는데 리스트와 벡터이다. 리스트는 앞쪽에서 원소에 빠르게 접근하고 추가하거나 제거하는 것을 가능하게 한다. 벡터는 고정 크기 시퀀스로 모든 원소에 대해 상수 시간 인덱싱을 제공한다. 리스트와 벡터의 원소는 서로 다른 타입일 수 있다.
  • 리스트는 프로시저 listcons로 만들 수 있다. 프로시저 carcdr로 리스트 원소에 접근할 수 있다. 리스트는 다음과 같이 표기한다.
    [math((<원소1>\ <원소2>\ ...))]
  • 리스트 상수는 작은 따옴표 ' 한 개를 이름 앞에 붙여야 한다. 이렇게 해야 프로시저 호출로 오해 받지 않는다. 리스트를 다루는 연산은 아래와 같다.
    {{{> (define lst (list 1 2))
lst
(1 2)
(cons 0 lst) ;; 앞에 원소 추가하기
(0 1 2)
(append lst '(3 4)) ;; 이어 붙이기
(1 2 3 4)
(car lst) ;; 첫 번째 원소 얻기
1
(cdr lst) ;; 첫 번째 원소 제거하기
(2)
(cdr (cdr lst)) ;; 첫 번째와 두 번째 원소 제거하기
()
(length lst) ;; 리스트 길이 구하기
2
}}}
  • 벡터는 프로시저 vectormake-vector로 만든다. 프로시저 vector-refvector-set!로 벡터 원소에 접근할 수 있다. 벡터는 아래와 같이 표기한다.
    [math(\#(<원소1>\ <원소2>\ ...))]
  • 벡터를 다루는 연산은 아래와 같다.
    {{{> (define v (make-vector 5 42))
v
#(42 42 42 42 42)
(vector-set! v 2 #t) ;; 원소 수정하기
v
#(42 42 #t 42 42)
(vector-ref v 2) ;; 인덱싱
#t
(vector-length v) ;; 길이 구하기
5
}}}

3.4. 심볼

  • 심볼symbols은 식별자와 문법이 같은 문자열 상수이다. 예를 들어 foo*은 심볼이다. 리스트 상수와 같이 코드에서 심볼을 상수로 쓸 때는 작은 따옴표 ' 한 개를 이름 앞에 붙여야 한다. 이렇게 해야 심볼을 변수로 취급하지 않는다. 심볼은 리스트와 결합되어 매크로 처리, 코드 변환기 및 컴파일러에서 스킴 코드를 나타내거나 프로시저 eval로 평가할 때 특히 유용하다. 이렇게 하는 것을 s-expression이라고 한다.
    {{{> (string->symbol "foo") ;; conversion
foo
(symbol->string 'foo) ;; conversion
"foo"
(define x (list '* 2 3 7))
x
(* 2 3 7)
(eval x)
42
}}}

3.5. 프로시저

  • 스킴은 많은 연산을 제공한다. 이 연산은 미리 정의한 프로시저이고 변역 변수로 저장되어 있다. 예를 들어 전역 변수 *은 곱하기 프로시저이다. 새 프로시저는 폼 lambdadefine으로 만들 수 있다. 자세한 내용은 폼 섹션에서 설명한다.
  • 기본 섹션에서 설명한 프로시저 호출 방법 외에 아래와 같이 프로시저 apply를 이용해 인자 리스트로 호출할 수 있다.
    {{{> *
#<procedure #2 *>
(* 2 3 7)
42
(apply * (list 2 3 7))
42
}}}

4. 기본 폼

4.1. lambda

  • lambda 폼은 스킴에서 중심적인 역할을 한다. 이른바 람다 표현식은 다음과 같은 구문을 가진다.
    [math((lambda <parameter{\text -}list>\ <body>))]
  • <parameter-list> 인자 스코프는 <body>에 국한된다.
  • 프로시저가 고정된 개수의 인자를 받을 때 <parameter-list>는 매개변수 이름이 괄호로 묶인 리스트로 표현된다.
    {{{> (define fact

    • (lambda (n) ;; sole parameter is n

        (if (< n 2)

          1
          (* n (fact (- n 1))))))
(fact 30)
265252859812191058636308480000000
(define (fact n) ;; equivalent def.
(if (< n 2) 1 (* n (fact (- n 1)))))
}}}
  • 가변 개수의 인자를 받는 프로시저(가변 인자 프로시저)는 매개변수 리스트의 끝에 마침표로 접두된 나머지 매개변수를 포함한다. 마침표 앞에는 모든 필수 매개변수들이 위치한다. 나머지 매개변수는 필수 매개변수 외의 모든 인자를 리스트로 포함하게 되며, 인자가 없을 경우 빈 리스트가 된다.
    {{{> (define rot

    • (lambda (x . y)

        (append y (list x))))
(rot 1 2 3 4 5)
(2 3 4 5 1)
(define (rot x . y) ;; equivalent def.
(append y (list x)))
}}}
  • 스킴 변수는 렉시컬 스코프를 가진다. <body><parameter-list>에 선언되지 않은 변수를 참조하는 부분이 있을 때 이를 자유 변수free variable라고 한다. lambda 폼이 평가될 때 생성된 프로시저에서 자유 변수에 바인딩된 메모리 셀을 기억하며 이를 클로저closure라고 부른다. 프로시저가 호출될 때 자유 변수 참조는 이 메모리 셀에 접근한다. 이는 스킴 프로그래밍의 근본적인 측면으로 다양한 프로그래밍 패턴에서 사용될 수 있다. 예를 들어 다음과 같다.
    {{{> (define (adder n)

    • (lambda (x) (+ x n)))
(define add1 (adder 1))
(define sub1 (adder -1))
(add1 5) ;; 5 + 1
6
(sub1 (sub1 5)) ;; 5 + -1 + -1
3
}}}
  • 이 예시에서 adder를 호출할 때마다 생성된 클로저는 변수 n의 서로 다른 바인딩을 기억한다. 하나는 값 1을 포함하는 셀에 바인딩되고, 다른 하나는 값 -1을 포함하는 셀에 바인딩된다.
  • 클로저는 리스트의 각 요소에 대해 프로시저를 호출하는 mapfor-each와 같은 고차 프로시저를 사용할 때 특히 유용하다.
    {{{> (define (add-to-all n lst)

    • (map (lambda (x) (+ x n)) lst))
(add-to-all 10 '(1 2 3 4))
(11 12 13 14)
}}}

4.2. set!

  • set! 폼은 변수에 바인딩된 셀의 값을 변경한다.
    {{{> (define sum 0)
(for-each (lambda (n) (set! sum (+ sum n)))
'(1 2 3 4))
sum
10
}}}

4.3. if

if 폼은 조건부 평가를 수행한다. 첫 번째 인자는 조건 표현식이며 첫 번째 표현식의 값이 #f가 아닐 때 평가되는 두 번째 표현식이 뒤따른다. 선택적으로 첫 번째 표현식의 값이 #f일 때 평가되는 세 번째 표현식도 있을 수 있다.

5. Derived 폼

다른 표현식 형식은 기본 형식에 대한 문법 설탕으로 볼 수 있다.

5.1. let, let*, letrec

  • 이들은 로컬 변수를 생성하는 바인딩 폼이다. 이들은 동일한 구문을 공유하며 여기서는 let을 통해 설명한다.
    [math((let\ ((<var>\ <expr>)\ ...)\ <body>))]
  • 각 변수 <var>에 대해 바인딩 목록에 언급된 변수들마다 셀이 생성되고, 해당 변수들은 대응하는 표현식 <expr>의 값으로 초기화된다. let 폼의 경우, 변수들의 스코프는 표현식 <body>로 제한된다. let* 폼의 경우, 변수의 스코프는 바인딩 목록에서 뒤따르는 표현식들도 포함하며 표현식들은 왼쪽에서 오른쪽으로 차례대로 평가된다. letrec 폼은 변수의 스코프는 바인딩 목록에 있는 모든 표현식을 포함하며 이는 특히 재귀 프로시저 정의에 유용하다.
    {{{> (define x 10)
(let ((x (+ x 1)) (y (+ x 2))) (list x y))
(11 12)
(let* ((x (+ x 1)) (y (+ x 2))) (list x y))
(11 13)
(letrec ((x (lambda (n)
(if (> n 9) n (x (* n 2))))))(x 1))
16
x
10
}}}

5.2. begin

  • begin 폼은 임의의 개수의 하위 표현식을 받아들이며 왼쪽에서 오른쪽으로 차례로 평가하고 마지막 하위 표현식의 값을 반환한다.
    {{{> (define n -4)
(if (< n 0)
(begin
(display "neg\n")
(- n))
(begin
(display "pos\n")
(* 100 n)))
neg
4
}}}

5.3. and

  • and 폼은 임의의 개수의 하위 표현식을 받아들이며 왼쪽에서 오른쪽으로 차례로 평가하다가 하나라도 #f를 반환하면 평가를 멈춘다. 마지막으로 평가된 표현식의 값을 반환한다.
    {{{> (define n -4)
(and (>= n -10) (<= n 10) (* n n))
16
(and (> n 0) (sqrt n)) ;; sqrt not called
#f
}}}

5.4. or

  • or 폼은 임의의 개수의 하위 표현식을 받아들이며 왼쪽에서 오른쪽으로 차례로 평가하다가 하나라도 #f가 아닌 값을 반환하면 평가를 멈춘다. 마지막으로 평가된 표현식의 값을 반환한다.
    {{{> (define n -4)
(or (odd? n) (positive? n))
#f
(or (< n 0) (sqrt n)) ;; sqrt not called
#t
}}}

5.5. cond

  • cond 폼은 다중 조건 평가 폼이다. cond의 각 절은 조건과 해당 조건이 #f가 아닐 때 평가할 분기를 제공한다. 각 분기는 여러 표현식을 가질 수 있으며, 이는 암시적으로 begin 폼으로 감싸진다. cond 폼은 평가된 분기의 값을 반환한다.
    {{{> (define n 2)
(cond ((= n 0) "zero")
((= n 1) "one")
((= n 2) "two")
(else "other"))
"two"
}}}

5.6. case

  • case 폼은 값에 기반한 다중 분기 스위치이다. 각 분기는 하나 이상의 경우에 연결된다. 각 분기는 여러 표현식을 가질 수 있으며 이는 암시적으로 begin 폼으로 감싸진다. case 폼은 평가된 분기의 값을 반환한다.
    {{{> (define n 2)
(case n
((0 1) "small")
((2) "medium")
((3 4) "large")
(else "other"))
"medium"
}}}

6. 외부 링크

분류