조금 더 유지보수할 수 있도록 하기 위해 ML은 모듈을 제공 (교과서 8강, pdf 9강 참고)

Module

  • top level sequen of binding은 poor
    • shadowing earliear binding할 수 있기 때문,

→ ML use module

  • structure MyModule = struct bindings end
  • inside module
    • earlier binding 사용 가능(모듈 안이든 밖이든 다 가능함)
    • val, datatype, exception 등의 모든 binding type 가능
  • outside module
    • module binding을 refer하는 방법
    • Modulename.bindingName
    • ex) Int.max, List.foldl 이런거가 다 모듈인거임
  • 우리가 직접 모듈을 만들수도 있음

ex)

struct MyMathLib = 
struct
fun fact x = if x = 0 then 1 else x*(fact(x-1))
val half_pi = Math.pi/2.0
fun doubler x = x*2
end
// 사용하는 쪽
MyMathLib.fact 10;
MathMathLib.half_pi;

모듈을 제공하기에 이름이 헷갈리게 하지 않을 수 있음~

  • 사실 prefix로 모듈이름을 준다면 굳이 모듈할 필요가 없긴 함
  • namespace management역할을 해줌
  • hierarchy를 줘서 shadowing을 막아줌
  • 다른 모듈이 같은 이름을 reuse하는 것을 허용

Signature

  • module의 type
    • 어떤 binding을 가지고 어떤 타입인지 명시
  • signature을 정의하고 module을 ascribe
    • struct ModuleName >: signatureName
    • interface - implementation 관계

스크린샷 2023-06-11 오전 12.10.20.png

  • 시그니처는 타입에 가까움 어떤 바인딩을 가지고 있고 어떤 타입을 가지고 있는지를 알려줌

In general

signature

스크린샷 2023-06-11 오전 12.12.11.png

  • variable, type, datatype, exception 정의 가능

Ascribing a signature to a module

스크린샷 2023-06-11 오전 12.13.00.png

  • module은 signature와 match될 때까지 type check x
    • 즉, 모든 바인딩이 같은 타입이어야 함

    ex) signature MathLib에 있는 doubler가 있는데 structure MyMathLib에서 doubler가 주석처리되거나 타입이 다르다면 → 에러가 남. value type이 sinature과 맞지 않다

    • 하지만 more function은 가능하다!!
    • ex) 모듈에 시그니처에 없는 add(x,y) = x+ y 이런 함수를 모듈에 추가해도 에러 안남.
    • 다만 밖에서 access가 안됨!! 왜냐면 시그니처를 구독하게 했기 때문에(매치했기 때문에) 정의된 애들만 access가 되고 아닌 거는 access가 안됨

→ 시그니처는 hide thing to use user를 제공함을 알 수 있음: encapsulation

Hiding things

  • signature은 binding, type definition을 hiding할 수 있음
    • hiding implementation detail은 아주 중요한 특성 중 하나

Hiding with functions

스크린샷 2023-06-11 오전 12.15.26.png

  • 3개의 double fun은 동일한 역할이고 구현이 다르지만 유저는 알 필요가 없음
  • locally helper function를 정의하는 것은 powerful
    • 다른 코드에 영향을 받지 않으면서 함수를 fix를 하는데 좀 더 쉽게 할 수 있음
  • convenient to have private top level function
    • signature과 module에서 가리고 싶은 코드는 시그니처에는 선언하지 않고 모듈에만 선언한다면 유저는 접근할 수 없고 모듈 내에만 접근할 수 있기 때문에 hide implementation 가능
    • two functions could easily share a helper function
    • ML은 signature로 omit binding이 가능

doubler를 모듈에만 구현해서 가림

doubler를 모듈에만 구현해서 가림

Larger example

  • ADT를 정의하는 module을 만들어보자
  • add, toString을 지원하는 rational number 모듈

    rational은 유리수. b/a로 표현 가능 그리고 addition과 string representaion을 만들어볼거임

Libary spec (==property)

Property

  • 라이브러리가 보장하는 기능
  • externally visible guarantees, up to library writer
    • 분모가 0이 될 수 없다
    • string으로 출력 시 reduced form으로 줄 것(4/1 아니고 4, 9/6 아니고 3/2)
    • no infinite loops or exceptions

invariant(to make implemention simple)

  • 유저는 상관없고 이건 구현을 쉽게 하기 위함
  • part of implementation, not the module’s spec
    • 내부적으로는 분모를 0보다 크게 할 거임
      • 음수면 분자에 음수를 넣고 분모에는 항상 분모에 넣도록
    • rational value는 항상 reduced form으로 줄거임 string만이 아니라 항상 그럴거
      • 10/5라고 하면 toString을 할때만 2로 바꾸면 되지만 add나 만들 때에도 reduce해서 할거임

More on invariants

  • maintain
    • make_frac은 분모가 0이 되는 것을 막고 분모에 음수를 없애며 reduce result를 저장
    • add는 reduce form끼리 더한다고 가정
  • rely
    • gcd는 음수에는 동작하지 않을 것, 분자만 음수가 될 수 있기 때문에 분자에만 abs 이용
    • add는 최대한 reduce를 필요한데만 호출할 것
    • toString은 이미 reduce form일 것으로 가정

1) Rational1 >: RATIONAL_A (B,C)

signature RATIONAL_A = 
sig
datatype rational = Frac of int * int | Whole of int
exception BadFrac
val make_frac : int * int -> rational
val add : rational * rational -> rational
val toString : rational -> string
end
structure Rational1 :> RATIONAL_C = (* can ascribe any of the 3 signatures above *)
struct

(* Invariant 1: all denominators > 0
   Invariant 2: rationals kept in reduced form *)

  datatype rational = Whole of int | Frac of int*int
  exception BadFrac (*분모가 0 때의 exception*)

helper function인 gcd(양수 x,y의 최대공약수를 구하는 함수)와 reduce(rational을 받아서 reduce form을 생성) 함수를 생성

(* gcd and reduce help keep fractions reduced, 
   but clients need not know about them *)
(* they _assume_ their inputs are not negative *)
  fun gcd (x,y) =
       if x=y
       then x
       else if x < y
       then gcd(x,y-x)
       else gcd(y,x)

   fun reduce r =
       case r of
	   Whole _ => r
	 | Frac(x,y) => 
	   if x=0
	   then Whole 0
	   else let val d = gcd(abs x,y) in (* using invariant 1 *)
		    if d=y 
		    then Whole(x div d) 
		    else Frac(x div d, y div d) 
		end
  • make_frac: 두 int를 받아서 rational number를 생성, 분모가 0이면 exception 발생
    • 분모가 양수면 그냥 바로 Frac만들어서 reduce하면 됨
    • 분모가 음수면 부호 다 바꿔서 하면 됨
(* when making a frac, we forbid zero denominators *)
(* enforce invariant 1 (denom > 0) *)
(* must handle div by zero *)
   fun make_frac (x,y) =
       if y = 0
       then raise BadFrac
       else if y < 0
       then reduce(Frac(~x,~y))
       else reduce(Frac(x,y))
  • add: rational 두개를 받아서 더한 후에 Reduce폼 만들기
(* using math properties, both invariants hold of the result
   assuming they hold of the arguments *)
   fun add (r1,r2) = 
       case (r1,r2) of
	   (Whole(i),Whole(j))   => Whole(i+j)
	 | (Whole(i),Frac(j,k))  => Frac(j+k*i,k)
	 | (Frac(j,k),Whole(i))  => Frac(j+k*i,k)
	 | (Frac(a,b),Frac(c,d)) => reduce (Frac(a*d + b*c, b*d))
  • toString: reduce form으로 생성하기 때문에 그냥 string으로 바로 바꾸면 됨. 물론 reduce form을 안하다가 toString에서만 바꿔서 해도 됨
(* given invariant, prints in reduced form *)
fun toString r =
case r of
	Whole i => Int.tostring i (* 1-> "1", 2-> "2" *)
	| Frac(a,b) => (Int .toString a) A " "^ (Int. toString b)(*1/2 =-> "1/2" *)

reduce를 add에 안하는 이유는 얘는 reduce 이미 되어있기에 안해도 됨(그냥 산수래 생각해보래)

Rational1.make_frac(3,4);
Rational1.add(v1,v2);
Rational1.toString(v1);

RATIONAL_A의 Problem

  • Rational1.rational type이 모듈에 구현되어있으므로 client가 make_frac이 아닌 직접적으로 생성자를 불러서 만들 수 있음
    • Rational1.Frac(1,0) : 분모 0 안됨
    • Rational1.Frac(3,~1) : 분모에 음수 안됨
    • Rational1.Frac(9,6) : reduced form으로 rational 타입 만들기 안됨

→ exception, infinite loops, wrong result가 일어날 수 있음

So hide more

  • Key idea
    • ADT는 무조건 concrete type defintion을 가려서 client가 invariant-violating value of type을 만들 수 없도록 해야 함

Q. signature에서 datatype을 삭제하면 해결될까?

스크린샷 2023-06-11 오전 12.33.58.png

A. no, 이렇게 시그니처에서 타입을 제거하면 rational type이 먼지 모르기 때문에 동작하지 않음

2) RATIONAL_B

  • signature에는 type foo 만 줘서 type이 존재하는 건 알려주지만 정확한 definition은 알려주지 않게 함
    • 즉, alias(type synonym)를 줘서 애를 유저는 rational의 자세한 거를 모르지만 rational이 있다는 것만 앎. 모듈이 해당 타입을 구체화한 거임
signature RATIONAL_B =
sig
**type rational** (* type now abstract *)
exception BadFrac
val make_frac : int * int -> rational
val add : rational * rational -> rational
val toString : rational -> string
end

structure Rational1 :> RATIONAL_B = ...

→ 유저는 시그니처를 통해서 접근하기에 실제 타입안에 있는 생성자를 쓰지 못하지만 모듈은 선언해서 사용할 수 있게 됨

  • user는 invariant와 property를 violate하지 못하게 됨
  • rational을 처음 만드는 방법은 Rational1.make_frac밖에 없음
  • hide constructor and patterns
    • user는 Rational1.rational이 데이터 타입인지 아닌지도 모르게 됨

Two Powerful way to use signature for hiding

  • deny binding exist
    • val binding, fun binding, constructors ..
  • make type abstract
    • client는 value를 직접 만들거나 직접 access하지 못하게 함
    • type을 abstract해줘서 데이터타입을 좀 도 추상화하고 hide certain important한 거를 가릴 수 있음

3) RATIONAL_C

  • rational datatype을 가려서 유저가 직접적으로 사용하는 것을 방지했음
    • 사실 whole은 안가려도 되고 frac만 가리면 됨!!
  • signature에 whole을 expose하자!
    • still rest of the datatype은 hiding
    • still does not allow using Whole as a pattern
signature RATIONAL_C =
sig
type rational (* type still abstract *)
exception BadFrac
**val Whole : int -> rational** 
   (* client knows only that Whole is a function *)
val make_frac : int * int -> rational
val add : rational * rational -> rational
val toString : rational -> string
end
  • 유저가 보기에는 whole은 함수처럼 보이지만 사실 생성자!

Signature matching

structure Foo :> BAR is allowed if

  • BAR에 있는 every non abstract type은 Foo에 specified된대로 제공한다면 allow
  • BAR에 있는 every abstact typ은 Foo에 some way로 제공한다면 allow
    • datatype or type synonym
  • BAR에 있는 Every val-binding은 Foo에서도 제공한다면 allow
    • more general and/or less abstract internal type
    • signature에서의 function a의 타입:int → int ←시그니처가 더 general하면 안됨
    • module에서의 function a의 타입: ‘a → ‘a ← 모듈이 더 general하지만 상관 x!
  • BAR에 있는 every exception은 Foo에서 제공한다면 allow
  • Foo는 더 많은 binding을 가져도 됨

Equivalent implementation

  • Abstaction 의 포인트는 클라이언트에게는 동일하지만 다른 구현을 허용하게 하는 것!
    • client가 다름을 볼 수 있다면 replace하기가 어려워지게 client는 모름
    • can improve/replace/choose implementations later

    ex) 파이썬의 hash function

now

  • RATIONAL_A, RATIONAL_B, RATIONAL_C를 가질 수 있는 모듈 두개를 더 만들 거임
    • 단, RATIONAL_B, RATIONAL_C에서만 동일!!

Rational2 :> RATIONAL_B,C

  • property는 동일하지만 keep rational reduced form for all function을 지키지 않음
    • reduction을 string 출력할 때만 함(계산이 느릴 수 있으니까??)
    • gcd, reduce를 로컬함수로 변경
(* this structure can have all three signatures we gave
   Rationa1, and/but it is /equivalent/ under signatures 
   RATIONAL_B and RATIONAL_C 

   this structure does not reduce fractions until printing
*)

(* Invariant 1: all denominators > 0
   Invariant 2: toString gives a reduced representation of
                the rational number*)

structure Rational2 :> RATIONAL_C (* or B or C *) =
struct
  datatype rational = Whole of int | Frac of int*int
  exception BadFrac

  (* make_frac and add *)
   fun make_frac (x,y) =
       if y = 0
       then raise BadFrac
       else if y < 0
       then **Frac(~x,~y)**//여기서 reduce 안해도 !! 그거만  예시랑 다른 !
       else **Frac(x,y)**

(* using math properties, both invariants hold of the result
   assuming they hold of the arguments *)
   fun add (r1,r2) = 
       case (r1,r2) of
	   (Whole(i),Whole(j))   => Whole(i+j)
	 | (Whole(i),Frac(j,k))  => Frac(j+k*i,k)
	 | (Frac(j,k),Whole(i))  => Frac(j+k*i,k)
	 | (Frac(a,b),Frac(c,d)) => **(Frac(a*d + b*c, b*d)) //reduce 할 필요 x**

  (* toString (frac is reduced here) *)
   fun toString r =
   let //toString 함수에 nested function으로 gcd를 넣음
      fun **gcd** (x,y) =
           if x=y
           then x
           else if x < y
           then gcd(x,y-x)
           else gcd(y,x)
       fun **reduce** r =
           case r of
	       Whole _ => r
	     | Frac(x,y) => 
	       if x=0
	       then Whole 0
	       else let val d = gcd(abs x,y) in (* using invariant 1 *)
		        if d=y 
		        then Whole(x div d) 
		        else Frac(x div d, y div d) 
		    end
   in
       case reduce r of
            Whole x => Int.toString x
          | Frac(x, y) => Int.toString x ^ "/" ^ Int.toString y
   end
end
  • not equivalent under RATIONAL_A
    • rational_a를 가진다면 모든 폼이 reduce인 거를 보장하지 않기에 rational1과 rational2가 다름
      Rational1.toString(Rational1.Frac(9,6)); // make_frac아니고 Frac임!!!!!9/6
      Rational2.toString(Rational2.Frac(9,6)); // make_frac아니고 Frac임!!!!!3/2
    
    • rational_a를 가지지만 구현을 어떻게 하냐에 따라서 값이 다르게 됨
  • equivalent under RATIONAL_B,RATIONAL_C
    • different invariant but same properties

Rational3

  • 더 varaiation을 해보자! rational datatype을 쓰지 말고 튜플로 해보자!
    • type rational = int * int
(* this structure uses a different abstract type.  
   It does not even have signature RATIONAL_A.  
   For RATIONAL_C, we need a function Whole.  
*) 
structure Rational3 :> RATIONAL_C (* or C *)= 
struct 
   type rational = int * int
   exception BadFrac
	     
   (* int*int->int*int
      int*int->rational *)
   fun make_frac (x,y) =  // make_frac은 튜플을 받아서 튜플을 리턴
       if y=0
       then raise BadFrac
       else if y<0
       then (~x, ~y)
       else (x, y)

   fun Whole(i) = (i, 1) // whole함수는 int를 받아서 튜플을 리턴, RATIONAL_C때문에 필요
     (* 'a -> 'a*int 
     *
     * int -> rational
     *)

   fun add ((a,b),(c,d)) = (a*d + c*b, b*d)
     (* ((int*int) * (int*int)) -> (int*int) *)
     (*  (rational * rational ) -> rational *)

   fun toString (x,y) = // toString 시에 gcd를 선언해서 div해서 reduce form으로 변경
       if x=0
       then "0"
       else
	   let fun gcd (x,y) = 
		   if x=y
		   then x
		   else if x < y
		   then gcd(x,y-x)
		   else gcd(y,x)

	     val d = gcd (abs x,y)
	     val num = x div d
	     val denom = y div d
	   in
	     if denom=1
	     then Int.toString num
	     else Int.toString num ^ "/" ^ Int.toString denom
	   end
end
  • RATIONAL_A는 rational이 데이터타입이어서 전혀 쓸 수 없음
  • RATIONAL_B, RATIONAL_C에서 동일

Some interasting detail

  • make_frac
    • 내부적으로으로는 intint → int int이지만 클라이언트는 int*int → rational으로 보게 됨!!
    • 왜냐면 Rational_c는 make_frac가 rational 타입을 리턴하기 때문
    • Q. 그럼 rational → rational이 아닌 이유?
    • A. 만약 시그니처가 Rational-> rational로 된다면 유저는 rational에 뭐가 들어있는지 몰라서 쓸 수 없게 됨!
  • whole
    • 내부적으로는 ‘a → ‘a*int 이지만 클라이언트는 Int → rational임
    • 왜냐면 ‘a를 int로 바꾸게 되고 int* int가 rational이 되기 때문
    • 타입체커는 먼저 매칭을 한 후에 클라이언트는 int → rational로 보여줌
    • whole은 ‘a → int * int, ‘a → rational을 가질 수 없음. ‘a가 같이 specialize되어야 함
    • 내부적으로 함수 안에서는 whole함수를 string으로 해서 string* int 튜플로 할 수 있음

cf. client는 string을 넣으면 안되고 라이브러리 내에서는 string을 넣어도 상관없음

Can’t mix and match module

  • 같은 시그니처를 사용하는 모듈이여도 구현이 다르기 때문에 혼용해서 사용하면 안됨
  • 아래와 같은 경우 type check가 안될 것
Rational1.toString(Rational2.make_frac(9,6)); 
Rational1.toString(Rational2.make_frac(9,6));
  • type system and moduel property의 crucial feature
    • different modules have different internal invariant
    • different type definiiton
      • Rational1.rational이랑 Rationl2.rational은 비슷한 거 같지만 유저는 Rational3.rational이 int* int이고 data type이 아님을 알 수 없을 것!