본문으로 건너뛰기
code0.HDL

타입클래스와 proviso

타입클래스는 오버로드 연산의 묶음에 이름을 붙이고, 인스턴스는 타입별 구현을 제공하며, proviso는 다형 정의가 필요로 하는 인스턴스와 폭 관계를 선언하는데, 이 모두가 컴파일 시점에 결정되어 선택 회로를 만들지 않습니다.

이 장에서 배울 내용

  • 이스케이프 식별자로 쓰는 연산자 멤버를 포함해 타입클래스와 인스턴스를 선언합니다
  • 최소한의 정확한 proviso로 다형 함수를 작성하고 각 호출이 선택하는 인스턴스를 예측합니다
  • Add#(n, 4, m) 같은 크기 관계 proviso를 등식으로 읽고 numeric type을 valueOf로 값으로 바꿉니다
  • T0030, T0031, T0035에서 빠진 사실을 짚어 진단하고 deriving과 직접 작성한 Bits 인스턴스 가운데 하나를 고릅니다
이 장에서 배우는 개념
  • 타입클래스와 proviso
먼저 알아야 할 개념

+는 Integer, UInt#(8), Int#(16)에 모두 쓸 수 있지만, 세 타입이 같은 덧셈 구현을 공유하지는 않습니다. BSV는 같은 연산 이름에 타입별 구현을 연결하는 데 타입클래스를, 특정 타입의 구현을 제공하는 데 인스턴스를, 다형 정의가 필요로 하는 구현을 선언하는 데 proviso를 씁니다. 세 요소는 모두 타입 검사와 elaboration에서 처리되며, 실행 중인 회로에는 인스턴스를 고르는 논리가 생기지 않습니다. 이 장은 BSV Language Reference Guide 제8장의 타입클래스와 proviso, 그리고 제4장의 polymorphic type, proviso, numeric type 함수를 다룹니다. 타입·폭·명시적 변환 장의 폭과 pack, unpack, 사용자 정의 타입 장의 enum, 구조체, deriving, 그리고 변수·반복문·리터럴의 문맥 장에서 본 선언된 타입이 Literal 인스턴스를 고르는 방식을 알고 있어야 합니다.

1. 오버로딩과 타입클래스

1.1 다형성과 오버로딩

두 개념은 비슷해 보이지만 다릅니다.

개념구현 수예
다형 함수본문 하나를 여러 타입에서 구체화합니다function t pick(Bool c, t a, t b)
오버로드 연산타입마다 구현이 따로 있습니다Bit#(8)의 +와 Int#(16)의 +

타입클래스는 오버로드 연산의 묶음에 붙인 이름입니다. 예를 들어 Eq는 ==와 /=를, Bits는 pack과 unpack을, Arith는 +, -, * 같은 산술 연산을 묶습니다. 인스턴스는 특정 타입이 그 타입클래스의 연산을 어떻게 구현하는지 정의합니다.

1.2 타입 변수

polymorphic type은 구체적인 타입 대신 타입 변수를 매개변수로 씁니다. 타입 변수는 사용할 때마다 구체적인 타입 하나로 결정됩니다.

Vector#(n, t)   // 타입 t의 원소 n개. 모든 원소가 같은 타입입니다
List#(List#(b)) // 원소가 "타입 b의 리스트"인 리스트

타입 변수는 Bit#(n)의 n이나 위의 t처럼 소문자로 시작하며, 사용하는 문맥에서 결정됩니다. 회로가 실행되는 동안 정해지는 타입 변수는 없습니다.

1.3 컴파일할 때 일어나는 일

proviso가 붙은 정의를 호출하면 bsc는 다음 순서로 인스턴스를 찾습니다.

  1. 호출의 인자와 결과 선언에서 타입 변수와 폭을 추론합니다.
  2. 오버로드 연산과 proviso가 요구하는 조건을 모읍니다.
  3. 각 조건에 맞는 인스턴스나 numeric type 관계를 찾습니다.
  4. 찾은 인스턴스가 다른 조건을 요구하면 그 조건도 확인합니다.
  5. 선택한 구현으로 정의를 구체화하고 elaboration을 계속합니다.

이 과정은 rule의 조건도 시뮬레이션 사건도 아닙니다. 회로에는 태그, 멀티플렉서, 가상 호출이 추가되지 않습니다. 인스턴스를 찾으면 선택된 구현만 하드웨어 생성에 참여하고, 찾지 못하면 시뮬레이션 전에 컴파일 오류가 발생합니다.

2. 타입클래스 선언

2.1 타입클래스 선언 형식

형식

typeclass 클래스_이름#(type 타입_변수);
  멤버_선언;
  ...
endtypeclass
typeclass Describe#(type t);
  function Bit#(4) code(t value);
endtypeclass
부분의미
Describe타입클래스 이름입니다. 대문자로 시작합니다
type t이 타입클래스가 설명하는 타입 변수입니다
function Bit#(4) code(t value);모든 인스턴스가 구현해야 하는 멤버입니다. 선언만 있고 본문은 없습니다

멤버는 함수, 모듈, 일반 값일 수 있습니다. 타입클래스 선언 자체에는 구현이 없습니다.

2.2 상위 타입클래스 조건

타입클래스 선언에 provisos를 붙이면 그 타입클래스의 모든 인스턴스가 다른 타입클래스도 만족해야 합니다.

형식

typeclass 클래스_이름#(type t) provisos (상위_클래스#(t));
  ...
endtypeclass
typeclass Ranked#(type t) provisos (Eq#(t));
  function Bit#(2) rank(t x);
endtypeclass

Ranked 인스턴스가 있는 타입에는 반드시 Eq 인스턴스도 있어야 합니다. 인스턴스를 찾을 때 bsc는 이 조건까지 연쇄적으로 확인합니다.

3. 인스턴스 선언

3.1 인스턴스 선언 형식

인스턴스는 특정 타입에 대해 타입클래스의 모든 멤버를 정의합니다.

형식

instance 클래스_이름#(구체_타입);
  멤버_정의;
  ...
endinstance
instance Describe#(Bool);
  function Bit#(4) code(Bool value);
    return value ? 4'h1 : 4'h0;
  endfunction
endinstance
부분의미
instance Describe#(Bool);Bool 타입의 Describe 구현을 시작합니다
function ... code(Bool value);타입클래스의 멤버를 Bool에 맞게 정의합니다
endinstance인스턴스를 닫습니다

3.2 연산자 멤버 정의하기

== 같은 연산자를 멤버로 정의할 때는 이스케이프 식별자를 씁니다. 백슬래시로 시작하고 공백으로 끝나는 이름입니다. Eq의 두 멤버는 \==와 \/=입니다.

typedef struct { Bit#(4) v; } Code deriving (Bits);

instance Eq#(Code);
  function Bool \== (Code a, Code b) = a.v == b.v;
  function Bool \/= (Code a, Code b) = a.v != b.v;
endinstance

주의. \== 뒤의 공백은 식별자를 끝내는 BSV 문법의 일부입니다. 공백을 지우고 \==(Code a, ...)로 쓰면 괄호까지 식별자에 포함되어 구문 오류 P0005가 발생합니다.

4. proviso로 다형 함수 제약하기

4.1 proviso 형식

타입 변수 t가 무엇인지 모르면 t에 어떤 연산을 쓸 수 있는지도 알 수 없습니다. 그래서 다형 함수가 오버로드 연산을 사용하려면, 그 연산이 있는 타입만 받겠다고 provisos로 선언해야 합니다.

형식

function 반환_타입 이름(인자) provisos (클래스#(타입_변수), ...);
  ...
endfunction
function Bool same(t a, t b) provisos (Eq#(t));
  return a == b;
endfunction

Eq#(t)는 모든 타입에 비교 연산을 만들어 주지 않습니다. same을 호출할 때마다 Eq 인스턴스가 이미 있는 타입을 넘겨야 한다는 계약입니다. 이 계약이 있으므로 본문은 다형성을 유지하면서 ==를 쓸 수 있습니다.

호출필요한 인스턴스결과
same(8'd3, 8'd3)Eq#(Bit#(8))프렐류드에 있으므로 컴파일됩니다
same(True, False)Eq#(Bool)프렐류드에 있으므로 컴파일됩니다
Eq가 없는 타입해당 Eq 인스턴스컴파일 오류 T0031이 발생합니다

proviso는 타입 변수에 폭을 연결할 수도 있습니다.

function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
  return pack(x);
endfunction

Bits#(t, n)은 "t는 n비트 표현을 가지며 pack과 unpack을 쓸 수 있다"는 조건입니다. 이 proviso를 빼면 pack(x)를 쓸 근거가 없으므로 T0030 오류가 발생하고, 메시지는 Bits#(t, n)이 필요하다고 알려 줍니다.

proviso쓸 수 있는 기능
Bits#(t, n)t와 Bit#(n) 사이의 pack, unpack
Eq#(t), Ord#(t)동등 비교, 순서 비교
Arith#(t)산술 연산
Literal#(t)fromInteger를 통한 정수 리터럴
Bounded#(t)minBound, maxBound
BitExtend#(n, m, f)f#(n)에서 f#(m)으로의 폭 확장과 반대 방향의 절단

4.2 예제: 여러 타입에 쓰는 함수 하나

Polymorphism.bsv의 bitsOf는 Bits 인스턴스가 있는 어떤 타입이든 받아 비트 표현을 반환합니다. rule에서 Bool, UInt#(3), Bit#(4) 값으로 한 번씩 호출합니다.

실행하기 전에 예측해 보세요. 각 호출에서 t와 n은 무엇으로 결정될까요? 출력되는 비트 수는 몇 개일까요?

확인 문제

UInt#(3) reading = 5; 일 때 bitsOf(reading)은 무엇을 출력할까요?

답

Polymorphism.bsv
package Polymorphism;

// 구체적인 타입은 아직 정해지지 않았다. proviso는 pack을 요구하고 타입의 비트 폭을 n과 연결한다.
function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
  return pack(x);
endfunction

(* synthesize *)
module mkTop(Empty);

  rule show;
    Bool     flag   = True;
    UInt#(3) reading = 5;
    Bit#(4)  nibble = 4'b1010;

    // 각 호출에서 t가 따로 결정되고, 그 타입에 따라 폭이 정해진다.
    $display("Bool     -> %b", bitsOf(flag));
    $display("UInt#(3) -> %b", bitsOf(reading));
    $display("Bit#(4)  -> %b", bitsOf(nibble));
    $finish(0);
  endrule

endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄tn확인할 내용
BoolBool1True가 1 한 비트로 출력됩니다
UInt#(3)UInt#(3)35가 101로 출력됩니다
Bit#(4)Bit#(4)41010이 그대로 출력됩니다

함수 정의는 하나지만 t와 n은 각 호출에서 따로 결정됩니다. 첫 호출에서 정해진 폭이 다음 호출에 이어지지 않습니다.

직접 바꿔 보세요. Integer 값을 bitsOf에 전달해 보세요. Integer에는 Bits 인스턴스가 없으므로 T0031 오류가 발생하고, 메시지에 Bits#(Integer, ...) 인스턴스가 없다고 표시됩니다. 함수 본문을 고쳐서는 해결되지 않습니다. 비트 표현이 있는 타입을 전달해야 합니다.

4.3 예제: 인자 타입이 인스턴스를 고릅니다

PriorityInstance.bsv는 Priority 타입클래스와 두 인스턴스, 그리고 proviso를 가진 다형 함수 requestPriority를 정의합니다. 다음 순서로 읽으세요.

  1. Priority 선언과 멤버 priorityOf
  2. Bool 인스턴스와 RequestKind 인스턴스
  3. requestPriority의 provisos (Priority#(value_type))
  4. show의 두 호출

실행하기 전에 예측해 보세요.

  1. requestPriority(Write)와 requestPriority(True)는 각각 어느 인스턴스를 사용할까요?
  2. 두 줄에 출력되는 우선순위는 각각 얼마일까요?
  3. 회로가 실행되는 동안 인스턴스 선택이 바뀔 수 있을까요?

확인 문제

requestPriority(Write)는 어느 인스턴스를 사용하고 무엇을 출력할까요?

답

PriorityInstance.bsv
package PriorityInstance;

typedef enum { Reset, Read, Refresh, Write } RequestKind
  deriving (Bits, Eq, FShow);

// Priority는 오버로드 연산 하나를 묶는다. 각 instance는 자기 타입에 맞는
// priorityOf를 구현해야 한다.
typeclass Priority#(type value_type);
  function Bit#(2) priorityOf(value_type value);
endtypeclass

// 두 구체적인 instance가 같은 연산 이름에 타입별로 다른 동작을 제공한다.
instance Priority#(Bool);
  function Bit#(2) priorityOf(Bool value);
    return value ? 2'd3 : 2'd0;
  endfunction
endinstance

instance Priority#(RequestKind);
  function Bit#(2) priorityOf(RequestKind value);
    case (value)
      Refresh: return 2'd0;
      Read:    return 2'd1;
      Write:   return 2'd2;
      Reset:   return 2'd3;
    endcase
  endfunction
endinstance

// 다형 본문 하나가 인자 타입이 선택한 Priority instance를 사용한다.
function Bit#(2) requestPriority(value_type value)
  provisos (Priority#(value_type));
  return priorityOf(value);
endfunction

(* synthesize *)
module mkTop(Empty);
  rule show;
    $display("Write priority = %0d", requestPriority(Write));
    $display("True priority  = %0d", requestPriority(True));
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄결정된 value_type필요한 인스턴스확인할 내용
Write priorityRequestKindPriority#(RequestKind)Write에 2가 반환됩니다
True priorityBoolPriority#(Bool)True에 3이 반환됩니다

두 호출은 컴파일할 때 각자 다른 구현으로 구체화되었습니다. 실행할 때 rule show는 이미 정해진 연산을 수행할 뿐이고, 인스턴스를 고르는 회로는 없습니다.

직접 바꿔 보세요.

바꿀 곳예상 결과
requestPriority(True)를 requestPriority(False)로같은 Bool 인스턴스가 쓰이고 결과만 0으로 바뀝니다
True를 UInt#(8)'(5)로컴파일 오류 T0031이 발생합니다. Priority#(UInt#(8)) 인스턴스가 없다는 메시지입니다
requestPriority의 provisos 절 삭제컴파일 오류 T0030이 발생합니다. proviso가 너무 일반적이라며 필요한 Priority#(value_type)을 알려 줍니다

값을 바꾸면 같은 인스턴스 안에서 결과만 달라지고, 타입을 바꾸면 필요한 인스턴스 자체가 달라집니다.

5. 크기 관계 proviso

5.1 폭 사이의 관계 선언하기

일부 타입클래스는 함수 본문 대신 numeric type 사이의 등식을 나타냅니다. 이런 proviso는 입력 폭과 출력 폭이 어떤 관계인지 함수 시그니처에 적습니다. 실행 중의 산술이 아니라 numeric type 사이의 관계를 선언합니다.

형식

function Bit#(m) 이름(Bit#(n) x) provisos (Add#(n, k, m));
proviso뜻예
Add#(a, b, c)a + b = cAdd#(3, 4, 7)
Mul#(a, b, c)a × b = cMul#(3, 2, 6)
Div#(a, b, c)⌈a / b⌉ = cDiv#(7, 2, 4)
Max#(a, b, c)c = a와 b 중 큰 값Max#(3, 5, 5)
Log#(a, b)b = ⌈log₂ a⌉Log#(5, 3), Log#(8, 3)
function Bit#(m) twice(Bit#(n) x) provisos (Mul#(n, 2, m));
  return {x, x};
endfunction

twice(3'b101)을 Bit#(6)에 받으면 n = 3, m = 6이고 Mul#(3, 2, 6)이 성립하므로 101101이 됩니다.

function Bit#(m) pad0101(Bit#(n) x) provisos (Add#(n, 4, m));
  return { x, 4'b0101 };
endfunction

이 시그니처는 "반환 폭 m은 인자 폭 n보다 4 크다"는 폭 방정식입니다.

참고. 반대로 타입 안에서 폭을 계산할 때는 TAdd#(n, 1), TMul#(n, 2) 같은 타입 함수를 씁니다. proviso는 관계를 조건으로 요구하고, 타입 함수는 결과 타입을 직접 만듭니다.

5.2 예제: 인자 폭에 따라 달라지는 반환 폭

types 예제 묶음의 WidthProviso.bsv는 pad0101을 4비트 nibble과 2비트 pair로 한 번씩 호출합니다.

실행하기 전에 예측해 보세요. 두 호출에서 m은 각각 얼마일까요? 0101은 결과의 어느 쪽에 놓일까요?

확인 문제

Bit#(2) pair = 2'b11; 일 때 pad0101(pair)의 m과 결과는 무엇일까요?

답

WidthProviso.bsv
package WidthProviso;

// proviso가 m을 n + 4로 제약한다. 반환 폭은 인자 폭에 따라 정해진다.
function Bit#(m) pad0101(Bit#(n) x) provisos (Add#(n, 4, m));
  return { x, 4'b0101 };
endfunction

(* synthesize *)
module mkTop(Empty);

  rule show;
    Bit#(4) nibble = 4'b1100;
    Bit#(2) pair   = 2'b11;

    // 첫 호출에서 n은 4, 둘째에서 2다. 따라서 m은 8, 그다음 6이다.
    Bit#(8) widened = pad0101(nibble);
    Bit#(6) shorter = pad0101(pair);

    $display("pad0101(%b) = %b", nibble, widened);
    $display("pad0101(%b)   = %b", pair, shorter);
    $finish(0);
  endrule

endmodule

endpackage

결과 확인.

호출nm결과
pad0101(1100)4811000101
pad0101(11)26110101

두 결과 모두 입력이 상위 비트를, 0101이 하위 4비트를 차지합니다.

직접 바꿔 보세요.

바꿀 곳예상 결과
Bit#(6) shorter를 Bit#(8) shorter로T0031 오류가 발생하고, 메시지에 Add#(2, 4, 8) 인스턴스가 없다고 표시됩니다
Bit#(5) 변수에 pad0101(1'b1) 결과 담기n이 1, m이 5로 결정되어 10101이 됩니다

함수 정의는 그대로 유효합니다. 오류는 정의가 아니라 Add#(2, 4, m)과 m = 8을 동시에 만족할 수 없는 호출 위치에서 발생합니다.

5.3 예제: 결과 폭이 고정된 호출

typeclasses-and-provisos 예제 묶음의 WidthProviso.bsv에 있는 appendTag는 값 뒤에 4비트 태그를 연결합니다. 시그니처를 연결식보다 먼저 읽고, show에서 결과를 받는 선언 Bit#(7) result를 확인하세요.

실행하기 전에 예측해 보세요.

  1. n, 태그 폭, m은 각각 얼마일까요?
  2. bsc가 확인해야 하는 관계는 무엇일까요?
  3. 101과 1010을 연결한 7비트 값은 16진수로 얼마일까요?

확인 문제

Bit#(7) result = appendTag(3'b101, 4'ha); 일 때 tagged 줄은 무엇을 출력할까요?

답

WidthProviso.bsv
package WidthProviso;

function Bit#(m) appendTag(Bit#(n) value, Bit#(4) tag)
  provisos (Add#(n, 4, m));
  return { value, tag };
endfunction

(* synthesize *)
module mkTop(Empty);
  rule show;
    Bit#(7) result = appendTag(3'b101, 4'ha);
    $display("tagged = %h", result);
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

단계결정되는 것
첫 인자 3'b101n = 3
둘째 인자시그니처에 따라 4비트
결과 선언 Bit#(7)m = 7
확인할 관계Add#(3, 4, 7), 성립
실행 결과상위 101, 하위 1010을 연결한 1011010이 16진수 5a로 출력됩니다

직접 바꿔 보세요.

바꿀 곳예상 결과
결과 선언만 Bit#(8)로컴파일 오류 T0031이 발생합니다. Add#(3, 4, 8) 인스턴스가 없다는 메시지입니다. 이 proviso에는 자동 확장이나 절단이 없습니다
Bit#(8) result = appendTag(4'b1101, 4'ha);Add#(4, 4, 8)이 성립해 컴파일되고 da가 출력됩니다. 8비트 결과가 필요하면 입력 폭을 바꿔야 합니다

6. numeric type을 값으로 바꾸기

6.1 SizeOf와 valueOf

형식

SizeOf#(타입)          // 타입의 비트 폭을 numeric type으로 계산합니다
valueOf(numeric_type)  // numeric type을 Integer 값으로 바꿉니다
표현입력결과
SizeOf#(T)타입폭을 나타내는 numeric type
valueOf(n)numeric typeInteger 값
Integer w = valueOf(SizeOf#(Bit#(12)));   // 12

stringOf는 같은 방식으로 문자열 타입을 String 값으로 바꿉니다. valueof와 valueOf, stringof와 stringOf는 모두 쓸 수 있는 표기입니다.

6.2 예제: 타입의 폭 출력하기

TypeSize.bsv는 세 타입의 폭을 valueOf(SizeOf#(...))로 계산해 출력합니다. 식을 안쪽부터 읽으세요.

실행하기 전에 예측해 보세요. Bool, Bit#(12), UInt#(5)는 각각 몇 비트일까요?

확인 문제

UInt#(5) 줄은 무엇을 출력하며, valueOf는 무엇을 했을까요?

답

TypeSize.bsv
package TypeSize;

(* synthesize *)
module mkTop(Empty);

  rule show;
    // SizeOf#(T)는 값이 아니라 numeric type이다. valueOf로 Integer 값으로 변환한다.
    $display("Bool     occupies %0d bit(s)", valueOf(SizeOf#(Bool)));
    $display("Bit#(12) occupies %0d bit(s)", valueOf(SizeOf#(Bit#(12))));
    $display("UInt#(5) occupies %0d bit(s)", valueOf(SizeOf#(UInt#(5))));
    $finish(0);
  endrule

endmodule

endpackage

결과 확인.

출력 줄확인할 내용
Bool1비트입니다
Bit#(12)12비트입니다
UInt#(5)5비트입니다

SizeOf가 계산한 numeric type을 valueOf가 Integer로 바꾸었기 때문에 $display에 전달할 수 있었습니다.

직접 바꿔 보세요. 한 줄에서 valueOf를 지우고 SizeOf#(Bool)을 바로 전달해 보세요. 식이 와야 할 위치에 타입 표기가 왔으므로 P0005 구문 오류가 발생합니다.

7. 타입클래스 진단 읽기

7.1 진단과 빠진 사실

오류 메시지에 나온 타입과 proviso를 먼저 구체적인 조건으로 다시 적어 보세요. 문구가 비슷해도 빠진 사실과 고칠 곳은 다릅니다.

오류 코드와 모양빠진 사실고칠 곳
T0031 구체 타입의 인스턴스가 없음그 타입에 요청한 타입클래스의 구현이 없습니다지원되는 타입을 쓰거나 인스턴스를 정의합니다
T0031 크기 관계가 성립하지 않음폭 등식이 거짓입니다인자 폭, 결과 폭, 함수 시그니처를 맞춥니다
T0030 proviso가 너무 일반적임다형 본문이 쓰는 연산을 시그니처가 요구하지 않습니다메시지가 알려 주는 proviso를 그 정의에 추가합니다
T0035 크기를 알 수 없는 비트 벡터어느 인스턴스나 폭이 필요한지 정할 타입 정보가 없습니다식이나 결과 선언에 타입을 명시하고 불필요한 확장, 절단을 없앱니다

7.2 같은 이름, 다른 원인

Priority#(t)처럼 타입 변수가 남은 조건이 보이면 다형 정의 자신의 proviso가 빠졌을 가능성이 큽니다. Priority#(UInt#(8))처럼 구체 타입이 보이면 타입은 이미 결정되었고 그 타입의 인스턴스가 없다는 뜻입니다. Add#(3, 4, 8)은 모호한 것이 아니라 완전히 결정된 거짓 관계입니다.

Bit#(4) y = 3;
Bit#(4) x = truncate(zeroExtend(y));   // T0035: 중간 폭을 정할 수 없습니다

위 코드에서 zeroExtend의 결과 폭은 어디에도 적혀 있지 않습니다. truncate와 zeroExtend 사이의 폭을 정할 정보가 없으므로 T0035가 발생합니다.

컴파일하기 전에 위 표를 다형 함수 하나에 적용해 보세요.

function t largerOf(t a, t b);
  return (a > b) ? a : b;
endfunction

확인 문제

UInt#(4) 값 두 개로 largerOf(p, q)를 호출합니다. bsc는 무엇을 보고하고, 무엇을 고쳐야 할까요?

답

확인 문제

provisos (Ord#(t))를 넣은 뒤, (Bits, Eq)만 derive한 구조체 Pair로 largerOf를 호출합니다. 이번에는 bsc가 무엇을 보고하고, 어디를 고쳐야 할까요?

답

8. 직접 작성한 Bits 인스턴스

8.1 Bits 타입클래스

Bits#(value_type, width)는 의미를 나타내는 타입 하나와 비트 폭 하나를 연결하고, 두 방향의 변환을 정의합니다.

형식

instance Bits#(타입, 폭);
  function Bit#(폭) pack(타입 value);
    ...
  endfunction
  function 타입 unpack(Bit#(폭) bits);
    ...
  endfunction
endinstance

value_type이 정해지면 width도 정해집니다. 반대로 폭만으로는 원래 타입을 알 수 없습니다. UInt#(16)의 폭은 16이지만, 16비트로 표현되는 타입은 그 밖에도 많습니다.

8.2 언제 직접 작성하는가

deriving (Bits)는 enum label에 0부터 차례로 인코딩을 배정하고 구조체 필드를 컴파일러가 정한 순서로 배치합니다. 이 배치를 써도 되면 deriving을 쓰고, 외부 프로토콜이나 외부 하드웨어가 다른 코드를 정해 두었다면 인스턴스를 직접 작성합니다.

경우방법
인코딩이 설계 내부에서만 쓰임deriving (Bits)
외부 규격이 코드를 고정함직접 작성한 Bits 인스턴스

참고. Bits가 없는 타입도 유효합니다. 다만 Reg에 저장하거나 분리 합성한 모듈의 인터페이스로 내보낼 수는 없습니다.

8.3 예제: 프로토콜 인코딩

CustomBits.bsv의 Command는 Bits를 deriving하지 않고, 인스턴스 안에서 pack과 unpack을 직접 정의합니다. 두 case를 하나의 프로토콜 표의 두 방향으로 읽으세요.

Command기본 인코딩이었다면프로토콜 코드
Idle0001
Start0110
Stop1011

실행하기 전에 예측해 보세요.

  1. pack(Stop)은 무엇을 출력할까요?
  2. unpack(2'b10)은 어느 enum label일까요?

확인 문제

pack(Stop)은 무엇을 출력할까요?

답

CustomBits.bsv
package CustomBits;

// 외부 wire 프로토콜이 다른 코드를 지정하므로 이 enum은 Bits를 derive하지 않는다.
typedef enum { Idle, Start, Stop } Command deriving (Eq, FShow);

instance Bits#(Command, 2);
  // pack은 의미 있는 값을 프로토콜 인코딩으로 변환한다.
  function Bit#(2) pack(Command value);
    case (value)
      Idle:  return 2'b01;
      Start: return 2'b10;
      Stop:  return 2'b11;
    endcase
  endfunction

  // unpack은 역방향 변환을 정의한다. 사용하지 않는 코드는 Idle로 처리한다.
  function Command unpack(Bit#(2) bits);
    case (bits)
      2'b10:  return Start;
      2'b11:  return Stop;
      default: return Idle;
    endcase
  endfunction
endinstance

(* synthesize *)
module mkTop(Empty);
  rule show;
    Command decoded = unpack(2'b10);
    $display("Stop code = %b", pack(Stop));
    $display("10 means  = ", fshow(decoded));
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄확인할 내용
Stop code기본 인코딩 10이 아니라 프로토콜 코드 11입니다. 직접 작성한 pack이 선택되었습니다
10 meansunpack이 10을 Start로 디코딩합니다

타입 검사는 두 함수가 모두 2비트를 쓴다는 사실만 확인합니다. pack과 unpack의 표가 서로 역함수인지는 확인하지 않습니다. Command의 모든 값 c에 대해 unpack(pack(c)) == c가 성립하는지는 따로 검증해야 합니다.

직접 바꿔 보세요.

  1. unpack(2'b10)을 unpack(2'b00)으로 바꿔 보세요. 프로토콜이 쓰지 않는 코드이므로 default 분기가 선택되어 Idle이 출력됩니다.
  2. 쓰지 않는 코드를 Idle이 아니라 오류로 다루고 싶다면 Command만으로는 부족합니다. 정상 명령과 디코딩 실패를 함께 나타내려면 Maybe#(Command) 같은 tagged union 결과 타입이 필요합니다.

9. deriving과 동형 래퍼

9.1 deriving으로 만들 수 있는 인스턴스

enum, 구조체, tagged union에 언어가 정한 표준 동작을 쓰려면 deriving으로 인스턴스를 생성합니다.

절생성되는 동작
deriving (Bits)표준 packed 표현: pack, unpack
deriving (Eq)구조적 비교: ==, !=
deriving (Bounded)구성 타입에 경곗값이 있을 때 minBound, maxBound
deriving (FShow)구조를 보여 주는 fshow 출력 형식

여러 항목은 deriving (Bits, Eq, FShow)처럼 함께 씁니다. typedef와 deriving 문법의 실행 예제는 사용자 정의 타입 장에 있습니다.

9.2 동형 래퍼

필드가 하나인 구조체와 멤버가 하나인 tagged union은 내부 타입과 같은 정보를 담으면서도 별개의 타입입니다. 이런 타입을 동형 래퍼라고 합니다. BSV는 래퍼에 내부 타입의 인스턴스를 deriving으로 생성할 수 있습니다.

형식

typedef struct { 내부_타입 필드; } 래퍼_이름 deriving (..., Literal, Arith);

Literal은 정수 리터럴을, Arith는 산술 연산을 래퍼에 그대로 제공합니다.

9.3 예제: 단위가 있는 정수

IsomorphicWrapper.bsv의 Credits는 UInt#(8) 필드 하나를 감싼 구조체입니다. 먼저 Credits 선언의 deriving 목록을 읽으세요.

실행하기 전에 예측해 보세요.

  1. Credits first = 5;처럼 리터럴을 바로 대입할 수 있을까요?
  2. first + second의 타입과 값은 무엇일까요?

확인 문제

Credits first = 5; Credits second = 7; 일 때 first + second의 타입과 값은 무엇일까요?

답

IsomorphicWrapper.bsv
package IsomorphicWrapper;

// 필드 하나인 구조체는 필드 타입과 동형이므로 호환되는 instance를 wrapper를 통해
// derive할 수 있다.
typedef struct {
  UInt#(8) amount;
} Credits deriving (Bits, Eq, FShow, Literal, Arith);

(* synthesize *)
module mkTop(Empty);
  rule show;
    // Credits는 UInt#(8)과 구별되지만 literal과 산술 동작은 필드의 구현을 재사용한다.
    Credits first = 5;
    Credits second = 7;
    Credits total = first + second;
    $display("total = ", fshow(total));
    $finish(0);
  endrule
endmodule

endpackage

결과 확인. 출력 줄에는 Credits 타입 이름, 필드 이름 amount, 값 12가 함께 보입니다. 두 리터럴 5와 7이 Credits로 만들어졌고, 더한 결과도 Credits 타입을 유지합니다. deriving한 Literal과 Arith가 내부 UInt#(8)의 동작을 그대로 썼기 때문입니다.

직접 바꿔 보세요. Credits first = 5;를 UInt#(8) raw = 5;와 Credits first = raw; 두 줄로 바꿔 보세요. 컴파일 오류 T0020이 발생합니다. 기대한 타입은 Credits이고 실제 타입은 UInt#(8)입니다. 리터럴과 산술은 공유하지만 두 타입은 여전히 섞어 쓸 수 없습니다.

10. 구현 방법 선택 기준

설계 의도선택부담
구현 하나를 분명한 이름으로 호출함일반 함수타입별 오버로딩이 없습니다
언어의 표준 표현과 의미를 그대로 씀deriving언어가 정한 동작을 따라야 합니다
여러 타입이 같은 연산 이름을 공유하되 특정 타입의 의미를 직접 정함직접 작성한 인스턴스왕복 변환 같은 의미 계약을 따로 검증해야 합니다
다형 본문이 특정 연산이나 폭 관계를 씀최소한의 proviso호출자도 같은 조건을 만족해야 합니다

큰 타입클래스는 관계없는 연산을 한 계약에 묶어, 호출자에게 쓰지 않는 인스턴스까지 요구할 수 있습니다. 작은 타입클래스와 좁은 proviso를 쓰면 다형 함수를 더 많은 타입에 적용할 수 있습니다.

11. 자주 하는 실수

11.1 다형 함수에 proviso를 빠뜨리기

// 잘못된 코드: 본문이 priorityOf를 쓰는데 시그니처가 요구하지 않습니다
function Bit#(2) requestPriority(value_type value);
  return priorityOf(value);
endfunction

// 바른 코드
function Bit#(2) requestPriority(value_type value)
  provisos (Priority#(value_type));
  return priorityOf(value);
endfunction
// 잘못된 코드: 타입 변수에 pack을 쓸 근거가 없습니다. T0030
function Bit#(n) bitsOf(t x);
  return pack(x);
endfunction

// 바른 코드
function Bit#(n) bitsOf(t x) provisos (Bits#(t, n));
  return pack(x);
endfunction

T0030 오류는 proviso가 너무 일반적이라고 보고하고, 필요한 조건 Priority#(value_type) 또는 Bits#(t, n)과 그 조건이 생긴 위치를 함께 알려 줍니다. 타입 변수에 쓰는 연산은 proviso로 선언해야 합니다. 메시지가 필요한 proviso를 그대로 보여 주므로, 그 내용만 시그니처에 추가하세요.

11.2 결과 폭만 바꾸기

// 잘못된 코드: Add#(3, 4, 8)은 거짓입니다
Bit#(8) result = appendTag(3'b101, 4'ha);

// 바른 코드: 관계가 성립하도록 입력 폭을 맞춥니다
Bit#(8) result = appendTag(4'b1101, 4'ha);

크기 관계 proviso에는 자동 확장이나 절단 규칙이 없습니다. 결과에 0을 채운 8비트가 필요하다면 appendTag의 결과를 Bit#(7)로 받은 뒤 zeroExtend로 넓히세요.

11.3 인스턴스가 없는 타입 넘기기

// 잘못된 코드: Priority#(UInt#(8)) 인스턴스가 없습니다
$display("%0d", requestPriority(UInt#(8)'(5)));

// 바른 코드: 그 타입이 정말 필요하면 인스턴스를 정의합니다
instance Priority#(UInt#(8));
  function Bit#(2) priorityOf(UInt#(8) value) = truncate(pack(value));
endinstance

T0031 오류에 구체 타입이 보이면 함수가 아니라 그 타입의 인스턴스가 없다는 뜻입니다. 관련 없는 proviso를 다형 함수에 추가해도 해결되지 않습니다.

11.4 연산자 멤버 이름을 틀리게 쓰기

// 잘못된 코드: Eq의 멤버 이름은 \!= 가 아닙니다
function Bool \!= (Code a, Code b) = a.v != b.v;

// 바른 코드
function Bool \/= (Code a, Code b) = a.v != b.v;

Eq의 부등 멤버는 \/=입니다. \!=로 쓰면 T0016 오류가 !=는 Eq의 필드가 아니라고 보고합니다. 이스케이프 식별자 뒤의 공백을 빠뜨리면 P0005 구문 오류가 발생합니다.

11.5 동형 래퍼에 내부 타입 값을 대입하기

// 잘못된 코드
UInt#(8) raw = 5;
Credits c = raw;

// 바른 코드: 구조체 생성식으로 감쌉니다
Credits c = Credits { amount: raw };

래퍼는 내부 타입과 다른 타입이므로 T0020 오류가 발생합니다. 래퍼를 쓰는 목적이 바로 이런 섞어 쓰기를 막는 것입니다.

11.6 valueOf 없이 numeric type 쓰기

// 잘못된 코드: 타입을 값 자리에 썼습니다. P0005
$display("%0d", SizeOf#(Bool));

// 바른 코드
$display("%0d", valueOf(SizeOf#(Bool)));

numeric type은 타입이므로 식에 쓰려면 valueOf로 Integer 값으로 바꿔야 합니다.

12. 정리

주제핵심
타입클래스오버로드 연산의 묶음입니다. 선언에는 멤버의 타입만 있습니다
인스턴스특정 타입이 타입클래스의 멤버를 구현합니다
proviso다형 정의가 필요로 하는 인스턴스나 폭 관계를 시그니처에 적습니다. 타입 변수는 호출마다 따로 결정됩니다
인스턴스 선택컴파일할 때 인자 타입으로 결정됩니다. 실행 중 선택 회로는 없습니다
크기 관계Add, Mul, Div, Max, Log가 폭 사이의 등식을 요구합니다
numeric typeSizeOf로 계산하고 valueOf로 Integer 값으로 바꿉니다
Bits 직접 작성외부 인코딩을 정확히 표현하지만 왕복 변환은 따로 검증해야 합니다
동형 래퍼별개의 타입을 유지하면서 내부 타입의 리터럴과 산술을 deriving합니다
진단T0031 인스턴스 없음, T0030 proviso 부족, T0035 타입 정보 부족

13. 확인 문제

  1. Priority#(RequestKind)는 어느 단계에서 선택되며, 실행 중에 인스턴스를 고르는 회로가 생길까요?

    정답 보기

    타입 검사와 elaboration 단계에서 인자 타입 RequestKind로 선택됩니다. 선택된 구현만 하드웨어로 만들어지므로 실행 중에 인스턴스를 고르는 회로는 없습니다.

  2. Bits#(t, n)은 연산과 폭을 어떻게 함께 제약할까요?

    정답 보기

    t에 pack과 unpack을 쓸 수 있게 하고, 동시에 t의 비트 표현 폭을 n과 연결합니다. 그래서 Bit#(n) 반환 타입의 폭이 호출할 때 전달한 타입으로 결정됩니다.

  3. Add#(n, 4, m)은 결과 타입을 Bit#(7)로 직접 적는 것과 무엇이 다를까요?

    정답 보기

    Bit#(7)은 한 가지 폭만 허용합니다. Add#(n, 4, m)은 입력 폭 n이 무엇이든 결과 폭이 n + 4라는 관계를 요구하므로 여러 입력 폭에 같은 함수를 쓸 수 있습니다.

  4. pad0101(pair)의 결과를 Bit#(8)에 담으면 어디에서 어떤 오류가 날까요?

    정답 보기

    함수 정의가 아니라 호출 위치에서 T0031 오류가 납니다. n이 2이므로 Add#(2, 4, m)은 m = 6을 요구하는데, 목적지는 m = 8을 요구합니다.

  5. T0030과 구체 타입이 보이는 T0031은 각각 어디를 고쳐야 할까요?

    정답 보기

    T0030은 다형 정의가 본문에서 쓰는 연산을 요구하지 않았다는 뜻이므로 그 정의에 proviso를 추가합니다. 구체 타입이 보이는 T0031은 그 타입의 인스턴스가 없다는 뜻이므로 다른 타입을 쓰거나 인스턴스를 정의합니다.

  6. SizeOf#(T)를 $display에 바로 전달할 수 없는 이유는 무엇일까요?

    정답 보기

    SizeOf#(T)는 값이 아니라 numeric type입니다. $display는 값을 받으므로 valueOf로 Integer 값으로 바꿔서 전달해야 합니다.

  7. 직접 작성한 Bits 인스턴스에서 타입 검사가 보장하는 것과 보장하지 않는 것은 무엇일까요?

    정답 보기

    pack과 unpack이 선언한 폭을 쓴다는 사실은 보장합니다. 두 함수가 서로 역함수라는 사실, 곧 unpack(pack(c)) == c는 보장하지 않으므로 따로 검증해야 합니다.

  8. 동형 래퍼에 Literal과 Arith를 deriving하는 방법이 알맞은 경우는 언제일까요?

    정답 보기

    단위를 타입으로 구분해 섞어 쓰기를 막고 싶지만, 산술의 의미는 내부 타입과 같아도 될 때입니다. 산술 의미를 바꿔야 한다면 인스턴스를 직접 작성합니다.

이 장의 실습 문제는 다형 함수, 인스턴스 선택, 폭 proviso, Bits 인스턴스 예제를 이어 갑니다. 첫 문제에서는 Bits proviso가 있는 다형 함수의 본문을 완성합니다. 두 번째 문제에서는 다형 Priority proviso가 사용할 RequestKind의 우선순위 동작을 정의해야 합니다. 세 번째 문제에서는 Add proviso가 결과 폭을 정한 연결식을 완성해야 합니다. 네 번째 문제에서는 pack이 정한 프로토콜 인코딩과 어긋나게 디코딩하는 곳이 있습니다. 각 문제에서 오류나 잘못된 출력이 인스턴스, 폭 관계, 의미 계약 중 어디에서 오는지 먼저 판단해 보세요.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지