조금 더 유지보수할 수 있도록 하기 위해 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 관계
- 시그니처는 타입에 가까움 어떤 바인딩을 가지고 있고 어떤 타입을 가지고 있는지를 알려줌
In general
signature
- variable, type, datatype, exception 정의 가능
Ascribing a signature to a module
- 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
- 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를 모듈에만 구현해서 가림
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해서 할거임
- 내부적으로는 분모를 0보다 크게 할 거임
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을 삭제하면 해결될까?
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이 아님을 알 수 없을 것!