0강 : 준비

1강 : 기본 문법

2강 : 꼬리재귀, 매개변수 다형성, 사용자 정의 타입

3강 : 커링, 스테이징, map, filter, fold

4강 : 컨티뉴에이션 패싱 스타일(CPS)

4.5강 : n-Bishops 문제


오늘은 잠시 문법으로 돌아가서 SML에서의 예외처리, 모듈, 함자를 다룰 것이다. 




1. 내장 예외


SML에 내장되어있는 예외(exception)를 보자.


1) Fail

가장 기본적인 exception이다.

raise Fail "unimplemented"

를 하면 실행이 멈추고 예외가 raise된다.


2) Div

1 div 0등을 할때 raise된다.


c) Match

패턴 매칭이 모든 경우의 수를 다 고려하지 않을때 raise된다.


d) Bind

다음과 같이 할 수 없는 바인딩을 강제로 할때 raise된다.

val x::xs = [] (* Bind exception *)
val "a" = "b" (* Bind exception *)




2. 사용자 지정 예외


사용자 지정 예외를 정의하는법은 다음과 같다. 

exception Error


exception 생성자도 함수처럼 일급 객체여서 그 자체로 값이다. 타입은 exn 이고. 따라서 Error : exn 이 된다.


exn은 exception의 약자이며 동시에 extensible(확장 가능한) 의 약자이기도 하다. 새로운 생성자를 추가하여 값을 확장할 수 있다는 뜻이다.


주의해야할게, Error 는 exn타입이지 exception은 아니다. 예외를 만드려면 raise 키워드를 써서 raise Error처럼 해야한다. 


이 raise는 exn -> ‘a인 함수처럼 작동하나 일급객체가 아니라서 정확히 말하면 함수는 아니다.


또 datatype처럼 예외에는 인자가 있을 수 있다.

exception Crash of int Crash : int -> exnCrash 5 : exn




3. handle을 이용한 흐름 제어


SML에는 try-catch와 비슷한 역할을 하는 handle이란 키워드가 있다. 문법은 다음과 같다.


표현식 e : t, e1 : t, …, en : t와 값 Exn1 : exn, Exn2 : exn, … ,ExnN : exn에 대하여, 표현식 e가 예외 ExnK를 raise한다면

e handle Exn1 => e1
       | Exn2 => e2 
      ...
       
| ExnN => en


는 ek로 계산된다.


handle을 이용하면 팩토리얼을 예외처리를 이용해 CPS처럼 구현할 수 있다.


exception Return of int
fun factException 0 = raise Return 1
  | factException n = factException (n - 1handle (Return res=> raise Return (res * n)

factException 6 handle Return x => x 

계산하면 제대로 720이 나온다. 


이게 왜 되는지는 4강에서 설명한것과 동일하니 넘어가도록 한다. 컨티뉴에이션 대신 handle이 들어간것 뿐이다. 




4. 모듈


이제 주제를 바꿔서 SML의 모듈 시스템에 대해 알아보자. C++ 등에서의 abstract class와 비슷한 개념이다.


문법은 다음과 같이 signature <abstract class이름>을 쓰고 sig와 end 사이에 virtual function을 쓰는 방식이다. 인터페이스를 정한다고 보면 된다.

signature GEOMETRY =
sig
     type point
     val origin : point
     val get_x : point -> real 
     
val get_y : point -> real
end


주어진 signature를 구현하고 싶다면 다음과 같이 structure <이름> : <signature 이름>을 쓰고 struct와 end 사이에 구현된 값을 쓴다.


structure는 주어진 signature의 모든 값을 구현해야 한다. 이때 signature에 있는 값보다 더 많은 값이 있어도 상관없다. (보조함수라던지…)


* 데카르트 좌표계 *)
structure RectGeo : GEOMETRY = 
struct
    type point = real * real

    val origin = (0.0, 0.0)    
    
fun get_x (x, y= x    
    
fun get_y (x, y= y
end

(* 극좌표계 *)
structure PolarGeo : GEOMETRY =
struct
   
    
(* d, theta *)   
    
type point = real * real

    val origin = (0.0, 0.0)
    
fun get_x (d, theta= d * (Math.cos theta)  
    
fun get_y (d, theta= d * (Math.sin theta)
end


structure에 구현된 함수를 호출할때는 <structure 이름>.<함수 이름> 처럼 .을 이용한다.


아 그리고 한가지 주의할점이 있는데, signature와 structure는 표현식이 아니라는 것이다. 


1강에서 왠만한건 다 표현식이라 했는데 이 둘은 표현식이 아니다. 따라서 값도 타입도 없으며 val x = RectGeo 는 컴파일되지 않는다.


다만 이 둘은 타입과 값의 관계와 유사하다. 주어진 값이 그 값의 타입을 만족해야 하는것과 같이, 주어진 structure도 그 signature가 명시한 값을 전부 구현해야 한다.




5. Transparent and Opaque Ascription


이건 인터넷에 쳐도 누가 이미 번역해놓은게 없어서 그냥 영어를 쓰겠다.


Structure A가 signature B를 구현한다고 했을때 A는 B에 속한다고 한다 (A ascribes to B)


signature를 만드는 프로그래머의 입장에서, 유저가 signature의 내부를 들여다보게 하지 못하게 하고 싶을때가 있다.


signature에 type point라는 추상 타입이 있고 그에 속하는 structure가 type point = real * real이란 구체적인 타입으로 구현했다 하자.


보통은 유저가 point = real * real인 사실을 알아도 상관없을때가 있다. 이때 이걸 transparent ascription(투명적으로 속함) 이라고 하고 structure PolarGeo : GEOMETRY처럼 :를 쓴다.


하지만 어떤때는 유저가 point가 구체적으로 어떤 타입일지 몰라야 좋을때가 있다. 너무 많은 정보를 알게되면 유저가 signature에 주어진 함수 말고 자체적으로 다른 함수를 쓸 수 도 있기 때문이다. 


이때 point가 어떤 타입인지 모르게 하기 위해 opaque ascription (불투명적으로 속함)이란걸 쓰고 structure PolarGeo :> GEOMETRY 처럼 :>를 쓴다.


Opaque ascription을 하면 유저는 signature에 있는 함수 말고 다른 함수를 그 추상타입에 쓸 수 없다.




6. 함자


위의 signature/structure 문법을 모듈이라고 하는데, 모듈을 자주 사용하다 보면 비슷한 모듈이 있을때 똑같은 코드를 몇번이고 쓰는 자신의 모습을 발견할 수 있을 것이다.


SML에서는 이런 중복을 최소화하려고 함자(functor)라는 문법을 지원한다. 간단히 말해서, 함자는 structure를 인자로 받아 새로운 structure를 만들 수 있다.


다음의 예시를 보자.

signature STACK =
sig
    type 'a t

    val push : 'a t -> 'a -> 'a t
    val pop : 'a t -> 'a option * 'a t
    val size : 'a t -> int
    val empty : 'a t
end

signature BOUNDED_STACK =
sig
    type 'a t

    exception Full
    val push : 'a t -> 'a -> 'a t
    val pop : 'a t -> 'a option * 'a t
    val size : 'a t -> int
    val empty : 'a t
end


STACK과 BOUNDED_STACK은 exception Full이 있냐 없냐 의 차이를 빼곤 정의가 같다. 


만약 STACK을 구현했는데 BOUNDED_STACK을 또 구현해야 한다면 어떨까? 우리가 현재 가진 문법으로는 다시 코드를 짤 수 밖에 없다. 


이때 등장하는게 함자이다.

functor BoundedStack (S : STACK:> BOUNDED_STACK =
struct
    type 'a t = 'a S.t

    val limit = 10

    exception Full

    fun push T x =
        if S.size T >= limit
        then raise Full
        else S.push T x

    fun pop T = S.pop T

    fun size T = S.size T

    val empty = S.empty
end


첫번째 줄을 보면 함자는 S라는 STACK structure를 인자로 받아서 BOUNDED_STACK structure를 만든다. 


함자 내부에서는 S를 이용해 대부분의 중복되는 코드를 구현하고 있다. 코드를 재사용한다고 볼수도 있다.


함자는 다음과 같이 추가적인 인자를 받을 수도 있다. 이 함자는 limit이라는 인자를 받는다. 

functor BoundedStack (structure S : STACK
                      val limit : int:> BOUNDED_STACK =
struct
    type 'a t = 'a S.t

    exception Full

    fun push T x =
        if S.size T >= limit
        then raise Full 
        
else S.push T x

    fun pop T = S.pop T

    fun size T = S.size T

    val empty = S.empty
end


SML에서의 함자는 보통 이렇게 모듈을 인자로 받아 다른 모듈을 쉽게 만드는 식으로 쓰인다. 


내가 함자에 대한 이론적인 배경이 없어서 잘 설명하지 못했는데, 혹시 함자에 대해 잘 알고 있는 사람이 있다면 댓글로 SML의 함자와 어떻게 다른지, 틀린점은 없는지 설명을 부탁한다. 


또 궁금하거나 이해 잘 안되는거 있으면 댓글로 물어보면 내가 아는선에서 최대한 답변해줄테니 많이 물어봐라.


오늘은 이렇게 유연하게 모듈을 만드는 방법을 알아봤다. 다음에는 오늘 배운걸 토대로 레드-블랙 트리를 구현해보자.


5.5강 : 레드-블랙 트리 구현

6강 : 시퀀스 자료구조와 병렬성

7강 : 지연된 연산, 무한 자료구조, 스트림

8강 : 명령형 프로그래밍

9강(完) : 컴파일러와 프로그램 분석


참고자료 1

참고자료 2