제네릭

제네릭 (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;

Tprivate로 선언하면 어떤 유한(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;

위 예시에서 형식 매개변수 TX를 실제 기존 요소, 즉 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 패키지를 제공해요. 한 예로 PutGet 같은 프로시저를 제공하는 제네릭 Float_IO 패키지가 있어요. 실제로 표준 라이브러리에서 사용 가능한 Float_Text_IOFloat_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_ColorsColor 타입에만 사용할 수 있어요. 하지만 이 알고리즘은 이론상 어떤 타입이든 — 열거 타입이든 많은 요소를 가진 복잡한 레코드 타입이든 — 사용할 수 있어요. 알고리즘 자체는 같고 타입만 다를 뿐이에요. 예를 들어 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 타입은 배열 타입을 추상화하고 TIndex 타입의 형식 선언을 사용해요.

예시: 테스트 애플리케이션 (Example: Test application)

이전 예시에서는 뒤집기 알고리즘 자체만 추상화하는 데 초점을 맞췄어요. 하지만 작은 테스트 애플리케이션도 추상화하기로 결정할 수 있었어요. 예를 들어 배열의 요소를 변경하는 다른 프로시저를 테스트하기로 결정한다면 유용할 수 있어요.

그러려면 추상화할 요소를 다시 선택해야 해요. 다음 형식 매개변수를 선언해요:

  • S: 배열 이름을 담은 문자열
  • 타입 T의 요소를 문자열로 변환하는 함수 Image
  • 배열에 어떤 연산을 수행하는 프로시저 Test

ImageTest는 형식 서브프로그램의 예시이고 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 프로시저를 참조해요.

더 알아보기 (Learn more)