제네릭
제네릭 (Generics)
제네릭(generics)은 Ada에서 메타프로그래밍에 사용돼요. 서로 공통 속성을 공유하는 추상 알고리즘에 유용해요. 서브프로그램이나 패키지가 제네릭일 수 있어요. 제네릭은 generic 키워드를 사용해 선언돼요.
출처: 제네릭 문서
본문
소개 (Introduction)
generic
type T is private;
-- Declaration of formal types and objects
-- Below, we could use one of the following:
-- <procedure | function | package>
procedure Operator (Dummy : in out T);
procedure Operator (Dummy : in out T) is
begin
null;
end Operator;
형식 타입 선언 (Formal type declaration)
형식 타입(formal type)은 특정 타입의 추상화예요. 예를 들어 어떤 정수 타입에서든, 또는 숫자 타입이든 아니든 어떤 타입에서든 동작하는 알고리즘을 만들고 싶을 수 있어요. 다음 예시는 Set 프로시저용 형식 타입 T를 선언해요.
generic
type T is private;
-- T is a formal type that indicates that
-- any type can be used, possibly a numeric
-- type or possibly even a record type.
procedure Set (Dummy : T);
procedure Set (Dummy : T) is
begin
null;
end Set;
T를 private로 선언하면 어떤 유한(definite) 타입이든 여기에 매핑할 수 있다는 뜻이에요. 하지만 선언을 제한해서 특정 타입만 그 형식 타입에 매핑되도록 허용할 수도 있어요. 몇 가지 예시:
| 형식 타입 | 형식 |
|---|---|
| 어떤 타입 | type T is private; |
| 어떤 이산형 타입 | type T is (<>); |
| 어떤 부동소수점 타입 | type T is digits <>; |
형식 객체 선언 (Formal object declaration)
형식 객체(formal object)는 서브프로그램 매개변수와 비슷해요. 형식 선언에서 선언된 형식 타입을 참조할 수 있어요. 예를 들어:
generic
type T is private;
X : in out T;
-- X can be used in the Set procedure
procedure Set (E : T);
procedure Set (E : T) is
pragma Unreferenced (E, X);
begin
null;
end Set;
형식 객체는 입력 매개변수이거나 in out 모드로 지정될 수 있어요.
제네릭 본문 정의 (Generic body definition)
제네릭 서브프로그램이나 패키지의 본문 선언에는 generic 키워드를 반복하지 않아요. 대신 실제 선언으로 시작하고 선언한 제네릭 타입과 객체를 사용해요. 예를 들어:
generic
type T is private;
X : in out T;
procedure Set (E : T);
procedure Set (E : T) is
-- Body definition: "generic" keyword
-- is not used
begin
X := E;
end Set;
제네릭 인스턴스화 (Generic instantiation)
제네릭 서브프로그램이나 패키지는 직접 사용할 수 없어요. 대신 인스턴스화해야 해요. new 키워드를 사용하며, 다음 예시에서 볼 수 있어요:
generic
type T is private;
X : in out T;
-- X can be used in the Set procedure
procedure Set (E : T);
procedure Set (E : T) is
begin
X := E;
end Set;
with Ada.Text_IO; use Ada.Text_IO;
with Set;
procedure Show_Generic_Instantiation is
Main : Integer := 0;
Current : Integer;
procedure Set_Main is new Set (T => Integer,
X => Main);
-- Here, we map the formal parameters to
-- actual types and objects.
--
-- The same approach can be used to
-- instantiate functions or packages, e.g.:
--
-- function Get_Main is new ...
-- package Integer_Queue is new ...
begin
Current := 10;
Set_Main (Current);
Put_Line ("Value of Main is "
& Integer'Image (Main));
end Show_Generic_Instantiation;
위 예시에서 형식 매개변수 T와 X를 실제 기존 요소, 즉 Integer 타입과 Main 변수에 매핑해 프로시저 Set을 인스턴스화해요.
제네릭 패키지 (Generic packages)
이전 예시들은 제네릭 서브프로그램에 초점을 맞췄어요. 이 절에서는 제네릭 패키지를 살펴볼게요. 구문은 제네릭 서브프로그램과 비슷해요. generic 키워드로 시작하고 형식 선언으로 이어져요. 유일한 차이는 서브프로그램 키워드 대신 package가 지정된다는 점이에요.
예시:
generic
type T is private;
package Element is
procedure Set (E : T);
procedure Reset;
function Get return T;
function Is_Valid return Boolean;
Invalid_Element : exception;
private
Value : T;
Valid : Boolean := False;
end Element;
package body Element is
procedure Set (E : T) is
begin
Value := E;
Valid := True;
end Set;
procedure Reset is
begin
Valid := False;
end Reset;
function Get return T is
begin
if not Valid then
raise Invalid_Element;
end if;
return Value;
end Get;
function Is_Valid return Boolean is (Valid);
end Element;
with Ada.Text_IO; use Ada.Text_IO;
with Element;
procedure Show_Generic_Package is
package I is new Element (T => Integer);
procedure Display_Initialized is
begin
if I.Is_Valid then
Put_Line ("Value is initialized");
else
Put_Line ("Value is not initialized");
end if;
end Display_Initialized;
begin
Display_Initialized;
Put_Line ("Initializing...");
I.Set (5);
Display_Initialized;
Put_Line ("Value is now set to "
& Integer'Image (I.Get));
Put_Line ("Resetting...");
I.Reset;
Display_Initialized;
end Show_Generic_Package;
위 예시에서 단일 요소만 가진 Element라는 간단한 컨테이너를 만들었어요. 이 컨테이너는 요소가 초기화되었는지 여부를 추적해요. 패키지 정의를 작성한 후 Element의 인스턴스 I를 만들고, 패키지 서브프로그램(Set, Reset, Get)을 호출해 인스턴스를 사용해요.
형식 서브프로그램 (Formal subprograms)
형식 타입과 객체에 더해 형식 서브프로그램이나 패키지도 선언할 수 있어요. 이 과정에서는 형식 서브프로그램만 설명하고, 형식 패키지는 고급 과정에서 다룰게요.
with 키워드로 형식 서브프로그램을 선언해요. 아래 예시에서 제네릭 프로시저 Check가 사용할 형식 함수(Comparison)를 선언해요.
generic
Description : String;
type T is private;
with function Comparison (X, Y : T)
return Boolean;
procedure Check (X, Y : T);
with Ada.Text_IO; use Ada.Text_IO;
procedure Check (X, Y : T) is
Result : Boolean;
begin
Result := Comparison (X, Y);
if Result then
Put_Line
("Comparison ("
& Description
& ") between arguments is OK!");
else
Put_Line
("Comparison ("
& Description
& ") between arguments is not OK!");
end if;
end Check;
with Check;
procedure Show_Formal_Subprogram is
A, B : Integer;
procedure Check_Is_Equal is new
Check (Description => "equality",
T => Integer,
Comparison => Standard."=");
-- Here, we are mapping the standard
-- equality operator for Integer types to
-- the Comparison formal function
begin
A := 0;
B := 1;
Check_Is_Equal (A, B);
end Show_Formal_Subprogram;
예시: I/O 인스턴스 (Example: I/O instances)
Ada는 표준 및 파생 타입용으로 인스턴스화할 수 있는 제네릭 I/O 패키지를 제공해요. 한 예로 Put과 Get 같은 프로시저를 제공하는 제네릭 Float_IO 패키지가 있어요. 실제로 표준 라이브러리에서 사용 가능한 Float_Text_IO는 Float_IO 패키지의 인스턴스이며 이렇게 정의돼요:
with Ada.Text_IO;
package Ada.Float_Text_IO is new Ada.Text_IO.Float_IO (Float);
부동소수점 타입의 어떤 객체와 함께든 직접 사용할 수 있어요. 예를 들어:
with Ada.Float_Text_IO;
procedure Show_Float_Text_IO is
X : constant Float := 2.5;
use Ada.Float_Text_IO;
begin
Put (X);
end Show_Float_Text_IO;
제네릭 I/O 패키지의 인스턴스화는 파생 타입에 유용할 수 있어요. 예를 들어 소수점 뒤 두 자리와 지수 없이 표시되어야 하는 새 타입 Price를 만들어 볼게요.
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Float_IO_Inst is
type Price is digits 3;
package Price_IO is new
Ada.Text_IO.Float_IO (Price);
P : Price;
begin
-- Set to zero => don't display exponent
Price_IO.Default_Exp := 0;
P := 2.5;
Price_IO.Put (P);
New_Line;
P := 5.75;
Price_IO.Put (P);
New_Line;
end Show_Float_IO_Inst;
Price_IO 인스턴스의 Default_Exp를 조정해 지수를 제거 하면, Price 타입 변수가 표시되는 방식을 제어할 수 있어요. 부수적으로 이런 식으로도 쓸 수 있었어요:
-- [...]
type Price is new Float;
package Price_IO is new
Ada.Text_IO.Float_IO (Price);
begin
Price_IO.Default_Aft := 2;
Price_IO.Default_Exp := 0;
이 경우 Put 호출 시 소수점 뒤 두 자리를 얻도록 Default_Aft도 조정해요.
제네릭 Float_IO 패키지에 더해 Ada.Text_IO에서 다음 제네릭 패키지를 사용할 수 있어요:
Enumeration_IO— 열거 타입용Integer_IO— 정수 타입용Modular_IO— 모듈러 타입용Fixed_IO— 고정소수점 타입용Decimal_IO— 십진 타입용
실제로 위 예시를 십진 타입으로 다시 쓸 수 있어요:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Decimal_IO_Inst is
type Price is delta 10.0 ** (-2) digits 12;
package Price_IO is new
Ada.Text_IO.Decimal_IO (Price);
P : Price;
begin
Price_IO.Default_Exp := 0;
P := 2.5;
Price_IO.Put (P);
New_Line;
P := 5.75;
Price_IO.Put (P);
New_Line;
end Show_Decimal_IO_Inst;
예시: ADT (Example: ADTs)
제네릭의 중요한 응용은 추상 데이터 타입(ADT)을 모델링하는 것이에요. 실제로 Ada에는 제네릭을 사용하는 수많은 ADT가 있는 라이브러리 Ada.Containers가 있어요. (컨테이너 절에서 설명.)
ADT의 전형적인 예는 스택이에요:
generic
Max : Positive;
type T is private;
package Stacks is
type Stack is limited private;
Stack_Underflow, Stack_Overflow : exception;
function Is_Empty (S : Stack) return Boolean;
function Pop (S : in out Stack) return T;
procedure Push (S : in out Stack;
V : T);
private
type Stack_Array is
array (Natural range <>) of T;
Min : constant := 1;
type Stack is record
Container : Stack_Array (Min .. Max);
Top : Natural := Min - 1;
end record;
end Stacks;
package body Stacks is
function Is_Empty (S : Stack) return Boolean is
(S.Top < S.Container'First);
function Is_Full (S : Stack) return Boolean is
(S.Top >= S.Container'Last);
function Pop (S : in out Stack) return T is
begin
if Is_Empty (S) then
raise Stack_Underflow;
else
return X : T do
X := S.Container (S.Top);
S.Top := S.Top - 1;
end return;
end if;
end Pop;
procedure Push (S : in out Stack;
V : T) is
begin
if Is_Full (S) then
raise Stack_Overflow;
else
S.Top := S.Top + 1;
S.Container (S.Top) := V;
end if;
end Push;
end Stacks;
with Ada.Text_IO; use Ada.Text_IO;
with Stacks;
procedure Show_Stack is
package Integer_Stacks is new
Stacks (Max => 10,
T => Integer);
use Integer_Stacks;
Values : Integer_Stacks.Stack;
begin
Push (Values, 10);
Push (Values, 20);
Put_Line ("Last value was "
& Integer'Image (Pop (Values)));
end Show_Stack;
이 예시에서 먼저 제네릭 스택 패키지(Stacks)를 만들고, 그다음 인스턴스화해 최대 10개의 정수 값을 가지는 스택을 만들어요.
예시: Swap (Example: Swap)
Color 타입의 변수를 교환하는 간단한 프로시저를 볼게요:
package Colors is
type Color is (Black, Red, Green,
Blue, White);
procedure Swap_Colors (X, Y : in out Color);
end Colors;
package body Colors is
procedure Swap_Colors (X, Y : in out Color) is
Tmp : constant Color := X;
begin
X := Y;
Y := Tmp;
end Swap_Colors;
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Non_Generic_Swap_Colors is
A, B, C : Color;
begin
A := Blue;
B := White;
C := Red;
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
New_Line;
Put_Line ("Swapping A and C...");
New_Line;
Swap_Colors (A, C);
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
end Test_Non_Generic_Swap_Colors;
이 예시에서 Swap_Colors는 Color 타입에만 사용할 수 있어요. 하지만 이 알고리즘은 이론상 어떤 타입이든 — 열거 타입이든 많은 요소를 가진 복잡한 레코드 타입이든 — 사용할 수 있어요. 알고리즘 자체는 같고 타입만 다를 뿐이에요. 예를 들어 Integer 타입의 변수를 교환하고 싶다면 구현을 중복하고 싶지 않아요. 따라서 이런 알고리즘은 제네릭을 사용한 추상화에 완벽한 후보예요.
아래 예시에서 Swap_Colors의 제네릭 버전을 만들고 Generic_Swap이라는 이름을 지어요. 형식 타입 T 선언 덕분에 이 제네릭 버전은 어떤 타입에서든 동작해요.
generic
type T is private;
procedure Generic_Swap (X, Y : in out T);
procedure Generic_Swap (X, Y : in out T) is
Tmp : constant T := X;
begin
X := Y;
Y := Tmp;
end Generic_Swap;
with Generic_Swap;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
procedure Swap_Colors is new
Generic_Swap (T => Color);
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Swap_Colors is
A, B, C : Color;
begin
A := Blue;
B := White;
C := Red;
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
New_Line;
Put_Line ("Swapping A and C...");
New_Line;
Swap_Colors (A, C);
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
end Test_Swap_Colors;
예시에서 볼 수 있듯, 제네릭 Generic_Swap 프로시저의 인스턴스로 선언함으로써 비제네릭 버전의 알고리즘과 같은 Swap_Colors 프로시저를 만들 수 있어요. 제네릭 T 타입이 Color 타입에 매핑되도록 Generic_Swap 인스턴스화에 인자로 전달해 지정해요.
예시: 뒤집기 (Example: Reversing)
두 값을 교환하는 알고리즘을 가진 이전 예시는 제네릭을 사용하는 가장 간단한 예시 중 하나예요. 다음으로 배열 요소를 뒤집는 알고리즘을 연구할게요. 먼저 Color 타입에 특화된 비제네릭 버전부터 시작할게요:
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It (X : in out Color_Array);
end Colors;
package body Colors is
procedure Reverse_It (X : in out Color_Array)
is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : Color;
X_Left : Color
renames X (I);
X_Right : Color
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Reverse_It;
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Non_Generic_Reverse_Colors is
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
for C of My_Colors loop
Put_Line ("My_Color: " & Color'Image (C));
end loop;
New_Line;
Put_Line ("Reversing My_Color...");
New_Line;
Reverse_It (My_Colors);
for C of My_Colors loop
Put_Line ("My_Color: " & Color'Image (C));
end loop;
end Test_Non_Generic_Reverse_Colors;
Reverse_It 프로시저는 색상 배열을 받아 시작으로 배열의 첫 요소와 마지막 요소를 교환하고, 배열 중간에 도달할 때까지 연속 요소로 그렇게 계속해요. 그 시점에 배열 전체가 뒤집혀 있어요. 테스트 프로그램의 출력에서 볼 수 있죠.
이 프로시저를 추상화하려면 알고리즘의 세 컴포넌트에 대한 형식 타입을 선언해요:
- 배열의 요소(예시의
Color타입) - 배열에 사용되는 범위(예시의
Integer범위) - 실제 배열 타입(예시의
Color_Array타입)
알고리즘의 제네릭 버전:
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
procedure Generic_Reverse (X : in out Array_T);
procedure Generic_Reverse (X : in out Array_T) is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : T;
X_Left : T
renames X (I);
X_Right : T
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Generic_Reverse;
with Generic_Reverse;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It is new
Generic_Reverse (T => Color,
Index => Integer,
Array_T => Color_Array);
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Reverse_Colors is
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
for C of My_Colors loop
Put_Line ("My_Color: "
& Color'Image (C));
end loop;
New_Line;
Put_Line ("Reversing My_Color...");
New_Line;
Reverse_It (My_Colors);
for C of My_Colors loop
Put_Line ("My_Color: "
& Color'Image (C));
end loop;
end Test_Reverse_Colors;
위에서 언급했듯 알고리즘의 세 컴포넌트를 추상화해요:
T타입은 배열의 요소를 추상화해요.Index타입은 배열에 사용되는 범위를 추상화해요.Array_T타입은 배열 타입을 추상화하고T와Index타입의 형식 선언을 사용해요.
예시: 테스트 애플리케이션 (Example: Test application)
이전 예시에서는 뒤집기 알고리즘 자체만 추상화하는 데 초점을 맞췄어요. 하지만 작은 테스트 애플리케이션도 추상화하기로 결정할 수 있었어요. 예를 들어 배열의 요소를 변경하는 다른 프로시저를 테스트하기로 결정한다면 유용할 수 있어요.
그러려면 추상화할 요소를 다시 선택해야 해요. 다음 형식 매개변수를 선언해요:
S: 배열 이름을 담은 문자열- 타입
T의 요소를 문자열로 변환하는 함수Image - 배열에 어떤 연산을 수행하는 프로시저
Test
Image와 Test는 형식 서브프로그램의 예시이고 S는 형식 객체의 예시라는 점에 주의하세요.
제네릭 Perform_Test 프로시저를 사용하는 테스트 애플리케이션 버전:
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
procedure Generic_Reverse (X : in out Array_T);
procedure Generic_Reverse (X : in out Array_T) is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : T;
X_Left : T
renames X (I);
X_Right : T
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Generic_Reverse;
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
S : String;
with function Image (E : T)
return String is <>;
with procedure Test (X : in out Array_T);
procedure Perform_Test (X : in out Array_T);
with Ada.Text_IO; use Ada.Text_IO;
procedure Perform_Test (X : in out Array_T) is
begin
for C of X loop
Put_Line (S & ": " & Image (C));
end loop;
New_Line;
Put_Line ("Testing " & S & "...");
New_Line;
Test (X);
for C of X loop
Put_Line (S & ": " & Image (C));
end loop;
end Perform_Test;
with Generic_Reverse;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It is new
Generic_Reverse (T => Color,
Index => Integer,
Array_T => Color_Array);
end Colors;
with Colors; use Colors;
with Perform_Test;
procedure Test_Reverse_Colors is
procedure Perform_Test_Reverse_It is new
Perform_Test (T => Color,
Index => Integer,
Array_T => Color_Array,
S => "My_Color",
Image => Color'Image,
Test => Reverse_It);
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
Perform_Test_Reverse_It (My_Colors);
end Test_Reverse_Colors;
이 예시에서 제네릭 프로시저(Perform_Test)의 인스턴스로 Perform_Test_Reverse_It 프로시저를 만들어요. 주의할 점:
- 형식
Image함수에Color타입의'Image애트리뷰트를 사용해요. - 형식
Test프로시저에 패키지의Reverse_It프로시저를 참조해요.