Ada 제네릭 프로그래밍 ── 타입으로 계약을 작성하고 재사용을 제로 코스트로 실현하기

· · Ada, 프로그래밍 언어, 제네릭, 타입 시스템, 정적 타이핑, 계약 모델, 제로 코스트 추상화, GNAT, Alire, 고신뢰성, 코드 재사용

1. 들어가며 ── ‘무엇이든 받는다’가 아니라 ‘무엇을 약속하는가’

정적 타입 언어로 재사용 가능한 코드를 작성하려 하면, 곧 같은 고민에 부딪힙니다. 정수용으로 작성한 스택을 문자열에도 쓰고 싶습니다. 부동소수점 배열에도 같은 통계 처리를 적용하고 싶습니다. 오름차순 정렬 로직을 내림차순 정렬에도 쓰고 싶습니다. 그런데 타입마다 같은 코드를 복사해 나가면 수정 누락이 생깁니다. 반대로 void*나 캐스팅으로 무엇이든 받는 설계를 하면 타입 안전성이 무너집니다.

Ada의 답은 제네릭(generic units)입니다.

Ada의 제네릭은 단순한 텍스트 치환이 아닙니다. 타입, 값, 서브프로그램, 그리고 패키지 자체를 가인수로 받아, 인스턴스화 시점에 정적으로 타입 검사가 이루어집니다. 즉, 실행 시점에 ‘이 타입으로 정말 괜찮은가’를 조사하는 것이 아니라, 컴파일 시점에 ‘이 부품이 이 계약을 만족하는가’를 확정하는 메커니즘입니다.

재사용하고 싶은 처리어떻게 재사용할 것인가복사와 붙여넣기void* / Object / 캐스팅Ada 제네릭수정 누락이 생기기 쉽다실행 시 오류나 타입 붕괴가 생기기 쉽다타입 안전컴파일 시점 검사실행 시 불필요한 디스패치 없음

이 글에서는 Ada의 제네릭 프로그래밍을 다음 흐름으로 정리합니다.

  • 총칭 서브프로그램
  • 총칭 패키지
  • 타입 파라미터, 값 파라미터, 서브프로그램 파라미터
  • private, range <>, digits <> 등의 타입 카테고리
  • 정렬, 스택, 통계 처리, Count_If, 키-값 저장소의 구현 예제
  • 정식 패키지 파라미터를 이용한 고차 제네릭
  • Ada의 contract model과 실무 설계 사고방식

이 주제는 이 블로그의 연재인 「Ada 언어의 매력」「SPARK를 이용한 형식 검증 입문」「안전한 동시성 처리」「리얼타임 시스템」에 이어지는 위치에 있습니다. Ada의 ‘타입으로 설계를 말한다’는 사고방식을, 제네릭이라는 관점에서 파고들어 봅니다.

2. 이 글의 지도

먼저 전체 그림을 다이어그램으로 파악합니다. Ada의 제네릭을 ‘타입을 인수로 받는 기능’으로만 이해하면 상당히 좁은 시각이 됩니다. 실제로는 재사용하고 싶은 단위에 따라 서브프로그램, 패키지, 서브프로그램 파라미터, 값 파라미터, 정식 패키지 파라미터를 조합합니다.

Ada GenericsGeneric SubprogramSwapCount_IfSortGeneric PackageStackStatisticsKV StoreFormal ParametersTypeprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxObjectMax_SizeThresholdSubprogramLess functionEquals functionPredicatePackagewith package P is newGenericDesign IdeasContract ModelStatic CheckingZero-Cost AbstractionSeparate Specification andBody

이 글을 읽는 방법은 단순합니다. 전반부에서는 문법을 따라가고, 후반부에서는 설계 판단을 다룹니다. Ada를 처음 접하는 분은 처음부터 세세한 문법을 외우려 하지 말고, ‘무엇을 가인수로 삼고 있는가’, ‘그 가인수에 어떤 연산을 허용하고 있는가’에 주목해 주십시오.

3. 실행 환경과 컴파일 방법

이 글의 코드는 GNAT 15.x 이상을 전제로 합니다. GNAT은 Ada를 대표하는 컴파일러로, Alire를 통해 설치할 수 있습니다. Alire는 Ada / SPARK의 패키지 매니저이며, 툴체인 관리나 빌드에도 사용할 수 있습니다.

gnat --version
# GNAT 15.2.1

GNAT은 Alire(Ada의 패키지 매니저)에서 alr install gnat_native gprbuild로 설치하고, PATH를 설정해 주십시오.

이 글에서 다루는 예제는 저장소 내에 다음과 같이 배치하는 것을 전제로 합니다.

ada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

여러 컴파일 단위를 하나의 파일로 모은 예제는 gnatchop으로 분할한 다음 gnatmake합니다.

mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo

-gnata는 어서션을 활성화하는 옵션입니다. 제네릭 자체를 사용하기 위해 필수는 아니지만, 학습용 예제에서는 계약이나 경계 조건을 확인하기 쉬워집니다.

실행 파일gnatmakegnatchop개발자실행 파일gnatmakegnatchop개발자하나의 .ada 파일을 전달.ads / .adb / main으로 분할gnatmake -gnata main바인드와 링크까지 실행./main실행 결과

4. Ada 제네릭의 기본 모델

Ada의 제네릭은 대략 다음 3단계로 생각하면 이해하기 쉽습니다.

  1. 총칭 단위를 작성한다
  2. generic 부분에 가인수를 작성한다
  3. 사용하는 쪽에서 new인스턴스화한다
generic 선언가인수총칭 본체new에 의한 인스턴스화일반 서브프로그램 또는 패키지로 사용타입서브프로그램패키지

예를 들어 두 값을 교환하는 처리를 제네릭으로 만들면, 타입만을 가인수로 삼을 수 있습니다.

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

이 시점에서는 Generic_Swap을 아직 호출할 수 없습니다. 이것은 ‘임의의 Element 타입에 대해 사용할 수 있는 교환 처리 템플릿’입니다. 구체적인 타입을 지정해야 비로소 일반적인 프로시저가 됩니다.

procedure Swap_Integer is new Generic_Swap (Integer);

다이어그램으로 나타내면 다음과 같은 관계입니다.

Integer를 전달Character를 전달My_Record를 전달Generic_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordInteger 변수를 교환Character 변수를 교환My_Record 변수를 교환

중요한 것은 템플릿 본체가 Element에 대해 사용할 수 있는 연산만으로 작성되어 있다는 점입니다. type Element is private;로 선언한 경우, 대입이나 동등 비교 같은 기본적인 연산은 사용할 수 있지만, 대소 비교나 산술 연산은 사용할 수 없습니다. 즉, 제네릭 선언 자체가 ‘이 부품이 무엇을 전제할 수 있는가’를 나타냅니다.

5. 가인수의 종류 ── Ada 제네릭의 어휘

Ada의 제네릭이 받을 수 있는 것은 타입만이 아닙니다. 이 점이 C#이나 Java의 일반적인 제네릭과 비교했을 때 크게 다른 부분입니다.

generic formal parameters타입 파라미터오브젝트 / 값 파라미터서브프로그램 파라미터패키지 파라미터type Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...with package P is new ...

대표적인 가인수를 표로 정리하면 다음과 같습니다.

종류 의미
타입 파라미터 type Element is private; 임의의 definite하고 non-limited한 타입을 받는 기본형
limited 타입 파라미터 type Element is limited private; 복사할 수 없는 타입도 받는다
이산형(discrete type) type Index is (<>); 정수형이나 열거형 등, 배열 첨자로 쓸 수 있는 타입
부호 있는 정수형 type Count is range <>; +, -, 대소 비교 등 정수 연산을 전제할 수 있다
모듈러 정수형 type Word is mod <>; 비트 연산이나 나머지 연산 성격의 정수를 다룬다
부동소수점형 type Real is digits <>; Float, Long_Float, 사용자 정의 부동소수점형 등
고정소수점형 type Money is delta <>; 고정소수점 연산을 다룬다
값 파라미터 Max_Size : Positive; 크기나 임계값 등을 인스턴스마다 고정한다
서브프로그램 with function Predicate (...) return Boolean; 비교 함수나 술어처럼 동작을 주입한다
패키지 with package P is new Some_Generic (<>); 이미 생성된 제네릭 패키지를 부품으로 받는다

이러한 어휘를 가지고 있기 때문에, Ada에서는 ‘무엇이든 받지만 내부에서 위험한 일을 한다’가 아니라 ‘이 연산이 가능한 타입만 받는다’는 방식의 작성이 자연스럽게 가능합니다.

6. 총칭 서브프로그램 ── Generic_Swap으로 최소 구성 이해하기

첫 번째 예로, 임의의 타입인 두 변수를 교환하는 Generic_Swap을 살펴봅니다.

6.1 명세부

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

generic에 이어지는 부분이 가인수입니다. 여기서는 Element라는 타입을 받습니다. is private는 제네릭 본체 입장에서 그 타입의 내부 표현을 알지 못한다는 의미입니다.

이 선언으로 알 수 있는 것은 다음 두 가지입니다.

  • Generic_Swap은 임의의 Element 타입에 대해 사용할 수 있다
  • 본체는 Element의 내부 구조나 대소 비교에는 의존하지 않는다

6.2 본체

procedure Generic_Swap (A, B : in out Element) is
   Temp : constant Element := A;
begin
   A := B;
   B := Temp;
end Generic_Swap;

이 본체에서는 Element에 대해 대입만을 사용하고 있습니다. A < B도, A + B도 사용하지 않습니다. 따라서 Integer, Character, 레코드형, 열거형 등 대입 가능한 타입이라면 자연스럽게 사용할 수 있습니다.

호출 후호출 전A = 20B = 10A = 10B = 20Temp = A

6.3 인스턴스화

사용하는 쪽에서는 new를 사용합니다.

procedure Swap_Int  is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);

이제 Swap_IntSwap_Char는 일반적인 프로시저로 호출할 수 있습니다.

with Ada.Text_IO; use Ada.Text_IO;

procedure Swap_Demo is
   generic
      type Element is private;
   procedure Generic_Swap (A, B : in out Element);

   procedure Generic_Swap (A, B : in out Element) is
      Temp : constant Element := A;
   begin
      A := B;
      B := Temp;
   end Generic_Swap;

   procedure Swap_Int is new Generic_Swap (Integer);

   X : Integer := 10;
   Y : Integer := 20;
begin
   Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
   Swap_Int (X, Y);
   Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;

실행 이미지는 다음과 같습니다.

Before: X= 10, Y= 20
After : X= 20, Y= 10

여기서 Swap_Int (X, Y);Float 변수를 넘길 수는 없습니다. Swap_IntInteger용으로 인스턴스화된 일반 프로시저이기 때문입니다. 제네릭을 ‘무엇이든 들어가는 구멍’이 아니라 ‘타입마다 안전한 구체물을 만드는 메커니즘’으로 파악하면 이해하기 쉬워집니다.

7. 총칭 패키지 ── 타입과 값을 파라미터로 삼기

서브프로그램 하나가 아니라 여러 연산과 내부 상태를 한데 묶어 재사용하고 싶을 때는 총칭 패키지를 사용합니다. 대표적인 예가 스택입니다.

스택은 요소 타입과 최대 크기만 바꾸면 기본 로직은 동일합니다.

Generic_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_Size=20

7.1 명세부

generic
   type Element_Type is private;
   Max_Size : Positive;
package Generic_Stack is
   procedure Push (Item : Element_Type);
   function Pop return Element_Type;
   function Is_Empty return Boolean;
   function Is_Full  return Boolean;
   function Size return Natural;

   Stack_Overflow  : exception;
   Stack_Underflow : exception;
end Generic_Stack;

여기서는 두 종류의 가인수를 사용하고 있습니다.

  • Element_Type은 타입 파라미터
  • Max_Size는 값 파라미터

Max_SizePositive이므로 0 이하의 크기로는 인스턴스화할 수 없습니다. 이처럼 값 파라미터에도 타입에 의한 제약을 부여할 수 있습니다.

7.2 본체

package body Generic_Stack is
   subtype Index_Type is Positive range 1 .. Max_Size;
   type Storage_Type is array (Index_Type) of Element_Type;

   Data : Storage_Type;
   Top  : Natural := 0;

   procedure Push (Item : Element_Type) is
   begin
      if Top = Max_Size then
         raise Stack_Overflow;
      end if;

      Top := Top + 1;
      Data (Top) := Item;
   end Push;

   function Pop return Element_Type is
      Result : Element_Type;
   begin
      if Top = 0 then
         raise Stack_Underflow;
      end if;

      Result := Data (Top);
      Top := Top - 1;
      return Result;
   end Pop;

   function Is_Empty return Boolean is
   begin
      return Top = 0;
   end Is_Empty;

   function Is_Full return Boolean is
   begin
      return Top = Max_Size;
   end Is_Full;

   function Size return Natural is
   begin
      return Top;
   end Size;
end Generic_Stack;

이 패키지 본체에서 중요한 점은, DataTop이 인스턴스마다 별도로 만들어진다는 것입니다.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float,   3);

이 둘은 같은 템플릿에서 만들어지지만 내부 상태는 공유하지 않습니다.

Float_Stack의 상태TopData : Float 배열Int_Stack의 상태TopData : Integer 배열Generic_StackInt_StackFloat_Stack

7.3 스택의 상태 전이

스택은 상태 기계로 보면 이해하기 쉽습니다.

PushPush / PopPop으로 마지막 요소를 꺼냄Push로 Max_Size에 도달PopPushPopEmptyNonEmptyFullOverflowUnderflow

7.4 사용 예제

with Ada.Text_IO; use Ada.Text_IO;

procedure Stack_Demo is
   generic
      type Element_Type is private;
      Max_Size : Positive;
   package Generic_Stack is
      procedure Push (Item : Element_Type);
      function Pop return Element_Type;
      function Is_Empty return Boolean;
      function Is_Full  return Boolean;
      function Size return Natural;
      Stack_Overflow  : exception;
      Stack_Underflow : exception;
   end Generic_Stack;

   package body Generic_Stack is
      subtype Index_Type is Positive range 1 .. Max_Size;
      type Storage_Type is array (Index_Type) of Element_Type;

      Data : Storage_Type;
      Top  : Natural := 0;

      procedure Push (Item : Element_Type) is
      begin
         if Top = Max_Size then
            raise Stack_Overflow;
         end if;

         Top := Top + 1;
         Data (Top) := Item;
      end Push;

      function Pop return Element_Type is
         Result : Element_Type;
      begin
         if Top = 0 then
            raise Stack_Underflow;
         end if;

         Result := Data (Top);
         Top := Top - 1;
         return Result;
      end Pop;

      function Is_Empty return Boolean is (Top = 0);
      function Is_Full  return Boolean is (Top = Max_Size);
      function Size     return Natural is (Top);
   end Generic_Stack;

   package Int_Stack is new Generic_Stack (Integer, 5);
begin
   Int_Stack.Push (10);
   Int_Stack.Push (20);
   Int_Stack.Push (30);

   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;

총칭 패키지는 실무에서 ‘작은 컨테이너’, ‘고정 길이 버퍼’, ‘링 버퍼’, ‘로그용 큐’, ‘하드웨어 추상화 계층’ 등에서 자주 효과를 발휘합니다. 특히 Ada에서는 크기를 실행 시에 유동적으로 다루기보다, 타입이나 값 파라미터로서 정적으로 고정하는 설계가 고신뢰성 시스템과 잘 맞아떨어집니다.

8. 가인수 서브프로그램 파라미터 ── 동작을 주입하기

타입만 받아서는 아직 표현할 수 없는 것이 있습니다. 예를 들어 정렬에서는 요소 타입뿐 아니라 ‘어느 쪽을 먼저 배치할 것인가’라는 비교 로직이 필요합니다.

Ada에서는 이 비교 함수를 제네릭의 가인수로 만들 수 있습니다.

Generic_Insertion_SortItem_TypeIndexItem_Array비교 함수표준 비교를 사용Greater를 전달해 내림차순독자적인 순서를 전달

8.1 명세부

generic
   type Item_Type is private;
   type Index is (<>);
   type Item_Array is array (Index range <>) of Item_Type;
   with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);

여기에는 4개의 가인수가 있습니다.

  1. Item_Type: 배열 요소의 타입
  2. Index: 배열 첨자의 타입
  3. Item_Array: 실제 배열형
  4. "<": 비교 함수

type Index is (<>);는 이산형을 받습니다. 정수형뿐 아니라 열거형도 받을 수 있습니다. 배열 첨자로 Positive만 쓰는 것이 아니라 Day 같은 열거형도 쓸 수 있다는 점이 Ada답습니다.

with function "<" ... is <>;is <>는 실인수를 생략했을 경우, 가시적인 표준 연산자나 적합한 함수를 사용한다는 의미입니다. 즉, Integer처럼 이미 <가 존재하는 타입이라면 비교 함수를 명시하지 않아도 사용할 수 있습니다.

8.2 본체

procedure Generic_Insertion_Sort (Items : in out Item_Array) is
   J   : Index;
   Key : Item_Type;
begin
   if Items'Length <= 1 then
      return;
   end if;

   for I in Index'Succ (Items'First) .. Items'Last loop
      Key := Items (I);
      J := I;

      while J > Items'First and then Key < Items (Index'Pred (J)) loop
         Items (J) := Items (Index'Pred (J));
         J := Index'Pred (J);
      end loop;

      Items (J) := Key;
   end loop;
end Generic_Insertion_Sort;

삽입 정렬은 큰 배열에는 적합하지 않지만, 제네릭을 설명하기에는 적합합니다. 비교 함수만 교체하면 같은 루프 구조를 오름차순에도 내림차순에도 사용할 수 있기 때문입니다.

YesNoNoYes정렬되지 않은 배열왼쪽부터 순서대로 Key를 꺼낸다Key가 바로 앞 요소보다 앞서는가?바로 앞 요소를 오른쪽으로 밀어낸다Key를 삽입끝까지 처리했는가?정렬된 배열

8.3 오름차순과 내림차순을 같은 본체에서 만들기

type Int_Array is array (Positive range <>) of Integer;

procedure Sort_Asc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array);

function Greater (Left, Right : Integer) return Boolean is
  (Left > Right);

procedure Sort_Desc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array,
   "<"        => Greater);

Sort_Asc는 표준 <를 사용합니다. 반면 Sort_Desc"<" => Greater로 비교 함수를 교체하고 있습니다.

99, 3, 47, 12Sort_Asc표준 비교Sort_DescGreater를 비교 함수로 전달3, 12, 47, 9999, 47, 12, 3

이 구조는 C++에서 비교 함수 객체를 템플릿 인수로 넘기는 설계나, Rust에서 트레이트 바운드를 사용해 순서를 요구하는 설계와 유사합니다. 다만 Ada에서는 가인수 서브프로그램 파라미터로서 ‘이런 형태의 함수를 전달한다’고 명시합니다.

9. 타입 카테고리 ── private보다 구체적인 계약 작성하기

type T is private;는 편리하지만 무엇이든 할 수 있는 것은 아닙니다. private한 타입에 대해서는 사칙연산이나 대소 비교를 당연하게 사용할 수 없습니다. 그래서 Ada에서는 가인수 타입 파라미터에 카테고리를 지정할 수 있습니다.

Formal Typeprivatelimited privatediscrete box: 이산형range box: 부호 있는 정수mod box: 모듈러 정수digits box: 부동소수점delta box: 고정소수점access 타입열거형정수형FloatLong_Float사용자 정의 부동소수점형

9.1 카테고리를 지정하면 무엇이 좋은가

예를 들어 평균이나 분산을 계산하려면 가산, 감산, 곱셈, 나눗셈이 필요합니다. private 타입으로는 이러한 연산을 전제할 수 없습니다. 그래서 부동소수점형으로 한정합니다.

generic
   type Real is digits <>;
   type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
   function Mean (Values : Real_Array) return Real;
   function Variance (Values : Real_Array) return Real;
end Generic_Statistics;

type Real is digits <>;에 의해 Real이 부동소수점형임을 알 수 있습니다. 따라서 제네릭 본체에서는 +, -, *, / 등을 사용할 수 있습니다.

9.2 본체

package body Generic_Statistics is
   function Mean (Values : Real_Array) return Real is
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         Sum := Sum + V;
      end loop;

      return Sum / Real (Values'Length);
   end Mean;

   function Variance (Values : Real_Array) return Real is
      M   : constant Real := Mean (Values);
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         declare
            D : constant Real := V - M;
         begin
            Sum := Sum + D * D;
         end;
      end loop;

      return Sum / Real (Values'Length);
   end Variance;
end Generic_Statistics;

9.3 FloatLong_Float로 사용하기

type Float_Array is array (Positive range <>) of Float;
type Long_Array  is array (Positive range <>) of Long_Float;

package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats  is new Generic_Statistics (Long_Float, Long_Array);

같은 통계 처리를 정밀도가 다른 부동소수점형에 대해 재사용할 수 있습니다.

Generic_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsFloat로 Mean / VarianceLong_Float로 Mean / Variance사용자 정의 Real로 Mean / Variance

9.4 카테고리 지정은 ‘타입 수준의 명세서’

카테고리 지정은 단순히 컴파일러를 조용히 시키기 위한 문법이 아닙니다. 읽는 사람에게도 ‘이 부품이 무엇을 요구하는가’를 전달하는 명세서가 됩니다.

작성하려는 처리 적합한 가인수 타입 이유
교환, 저장, 취출 private 대입만 가능하면 된다
복사할 수 없는 리소스 관리 limited private 대입을 전제하지 않는다
배열 첨자, 열거 상태의 순회 (<>) First, Last, Succ, Pred를 사용할 수 있다
정수의 합계, 카운터 range <> 정수 연산을 전제할 수 있다
비트마스크, 순환 카운터 mod <> 모듈러 연산을 전제할 수 있다
평균, 분산, 수치 계산 digits <> 부동소수점 연산을 전제할 수 있다
금액, 제어량 등의 고정 정밀도 delta <> 고정소수점 연산을 전제할 수 있다

10. 술어 주입 ── Count_If를 Ada답게 작성하기

가인수 서브프로그램 파라미터는 비교 함수뿐 아니라 술어(predicate)에도 사용할 수 있습니다. 술어란 값을 받아 Boolean을 반환하는 함수입니다.

C#의 Func<T, bool>, Java의 Predicate<T>, C++의 람다나 함수 객체와 비슷한 역할을, Ada에서는 제네릭의 가인수 서브프로그램으로 표현할 수 있습니다.

10.1 명세부

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
   with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;

여기서는 Predicateis <>를 붙이지 않았습니다. 표준으로 가시적인 술어 함수가 있는 것이 아니므로, 사용하는 쪽에서 반드시 전달하도록 하는 설계입니다.

10.2 본체

function Generic_Count_If (Arr : Array_Type) return Natural is
   Count : Natural := 0;
begin
   for Item of Arr loop
      if Predicate (Item) then
         Count := Count + 1;
      end if;
   end loop;

   return Count;
end Generic_Count_If;

처리 흐름은 단순합니다.

TrueFalse배열각 요소를 순회Predicate(Item)?Count를 증가아무것도 하지 않음다음 요소로Count를 반환

10.3 짝수 개수와 임계값 개수

type Int_Array is array (Positive range <>) of Integer;

function Is_Even (N : Integer) return Boolean is
  (N mod 2 = 0);

function Is_Large (N : Integer) return Boolean is
  (N > 50);

function Count_Even is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Even);

function Count_Large is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Large);

같은 순회 로직에서 조건만 다른 두 함수를 만들 수 있습니다.

Generic_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62짝수의 개수50보다 큰 개수

이 예제에서는 배열 순회, 카운터 관리, 결과 반환이 모두 공통입니다. 반면 ‘무엇을 셀 것인가’만을 함수로서 주입하고 있습니다. 이것이 Ada에서 말하는 고차적 설계의 기본형입니다.

11. 여러 파라미터의 합성 ── 범용 키-값 저장소

실제 부품에서는 타입 파라미터 하나만으로 끝나는 경우가 많지 않습니다. 키와 값의 타입, 키의 비교 방법, 최대 건수 등 여러 조건을 조합해야 합니다.

여기서는 고정 길이의 간단한 키-값 저장소를 예로 듭니다.

Generic_KV_StoreKey_TypeValue_Type키 일치 판정 함수Max_EntriesPut / Get / Contains설정값 저장소소규모 캐시임베디드용 고정 길이 딕셔너리

11.1 명세부

generic
   type Key_Type is private;
   type Value_Type is private;
   with function "=" (Left, Right : Key_Type) return Boolean is <>;
   Max_Entries : Positive := 50;
package Generic_KV_Store is
   procedure Put (Key : Key_Type; Val : Value_Type);
   function Get (Key : Key_Type) return Value_Type;
   function Contains (Key : Key_Type) return Boolean;

   Key_Not_Found : exception;
   Store_Full    : exception;
end Generic_KV_Store;

이 패키지에는 4개의 가인수가 있습니다.

파라미터 종류 역할
Key_Type 타입 키의 타입
Value_Type 타입 값의 타입
"=" 서브프로그램 키의 일치 판정
Max_Entries 최대 엔트리 수

Max_Entries에는 := 50으로 기본값을 부여하고 있습니다. 따라서 특별히 지정하지 않으면 50건짜리 저장소가 됩니다.

11.2 본체

package body Generic_KV_Store is
   subtype Index_Type is Positive range 1 .. Max_Entries;

   type Key_Array   is array (Index_Type) of Key_Type;
   type Value_Array is array (Index_Type) of Value_Type;
   type Used_Array  is array (Index_Type) of Boolean;

   Keys   : Key_Array;
   Values : Value_Array;
   Used   : Used_Array := (others => False);

   function Find_Index (Key : Key_Type) return Natural is
   begin
      for I in Index_Type loop
         if Used (I) and then Keys (I) = Key then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Index;

   function Find_Free return Natural is
   begin
      for I in Index_Type loop
         if not Used (I) then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Free;

   procedure Put (Key : Key_Type; Val : Value_Type) is
      Pos : Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         Pos := Find_Free;

         if Pos = 0 then
            raise Store_Full;
         end if;

         Used (Pos) := True;
         Keys (Pos) := Key;
      end if;

      Values (Pos) := Val;
   end Put;

   function Get (Key : Key_Type) return Value_Type is
      Pos : constant Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         raise Key_Not_Found;
      end if;

      return Values (Pos);
   end Get;

   function Contains (Key : Key_Type) return Boolean is
   begin
      return Find_Index (Key) /= 0;
   end Contains;
end Generic_KV_Store;

이 구현은 선형 탐색이므로 대량 데이터에는 적합하지 않습니다. 하지만 고정 길이・소규모・동적 메모리 할당 없음이라는 특성이 중요한 상황에서는 사용하기 쉬운 형태입니다.

Keys/Values/UsedGeneric_KV_Store 인스턴스호출 측Keys/Values/UsedGeneric_KV_Store 인스턴스호출 측alt[기존 키 있음][신규 키]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(Pos)

11.3 인스턴스화 예제

with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;

procedure KV_Demo is
   package Int_String_Store is new Generic_KV_Store
     (Key_Type    => Integer,
      Value_Type  => Unbounded_String,
      Max_Entries => 10);
begin
   Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
   Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));

   if Int_String_Store.Contains (1) then
      -- Get (1)로 값을 가져올 수 있다
      null;
   end if;
end KV_Demo;

"="은 생략했습니다. Integer에는 표준 동등 연산자가 있으며, is <>에 의해 그것이 사용되기 때문입니다.

만약 키가 대소문자를 구분하지 않는 문자열 등이라면, 독자적인 동등 함수를 전달할 수 있습니다.

function Same_Key (Left, Right : Unbounded_String) return Boolean is
  (To_Lower (To_String (Left)) = To_Lower (To_String (Right)));

package String_Key_Store is new Generic_KV_Store
  (Key_Type    => Unbounded_String,
   Value_Type  => Integer,
   "="         => Same_Key,
   Max_Entries => 100);

12. 정식 패키지 파라미터 ── 제네릭을 한층 더 부품화하기

Ada의 제네릭에서는 패키지 자체를 가인수로 만들 수 있습니다. 이를 이용하면 ‘어떤 제네릭 패키지에서 만들어진 인스턴스’를 다른 제네릭의 입력으로 다룰 수 있습니다.

Generic_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 스택을 받는 로거

예를 들어, 앞서 본 Generic_Stack의 인스턴스를 받아 그 크기를 출력하는 로거를 만든다고 합시다.

generic
   with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
   procedure Print_Size;
end Generic_Stack_Logger;

본체는 다음과 같습니다.

with Ada.Text_IO; use Ada.Text_IO;

package body Generic_Stack_Logger is
   procedure Print_Size is
   begin
      Put_Line ("Stack size =" & Natural'Image (Stack.Size));
   end Print_Size;
end Generic_Stack_Logger;

사용하는 쪽에서는 먼저 스택을 만들고, 그 스택을 로거에 전달합니다.

package Int_Stack is new Generic_Stack
  (Element_Type => Integer,
   Max_Size     => 10);

package Int_Stack_Logger is new Generic_Stack_Logger
  (Stack => Int_Stack);

이러한 설계를 통해 제네릭 부품들을 서로 조합할 수 있습니다.

2단계1단계Int_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

C++의 템플릿 템플릿 파라미터와 비슷한 용도이지만, Ada에서는 ‘이 제네릭 패키지의 인스턴스를 받는다’고 명시할 수 있습니다. 대규모 Ada 코드에서는 컨테이너, 알고리즘, 로그, 검사, 테스트 보조 등을 나누어 조합할 때 편리합니다.

13. Contract Model ── Ada 제네릭에서 가장 중요한 사고방식

Ada의 제네릭을 이해하는 데 있어 중요한 것이 contract model입니다.

제네릭 본체는 가인수가 약속한 연산만을 사용해 작성해야 합니다. 예를 들어 type Element is private;만 선언했는데 Element에 대해 <를 사용할 수는 없습니다. <를 사용하고 싶다면 가인수 서브프로그램으로 명시하거나, 타입 카테고리를 더 구체적으로 만들어야 합니다.

generic formal part계약generic body계약 범위 내에서 구현본체 단독으로 타입 검사인스턴스화actual parameters실제 타입・함수・값실인수가 계약을 만족하는지 검사일반 패키지/서브프로그램

이러한 설계에 의해 제네릭의 이용자뿐 아니라 제네릭을 작성하는 쪽도 보호받습니다.

13.1 C++ 템플릿과 다르게 보이는 점

C++ 템플릿은 강력하지만, 역사적으로는 ‘템플릿 본체를 인스턴스화해야 비로소 오류가 드러난다’는 성질이 있었습니다. C++20의 concepts로 개선되었지만, Ada의 제네릭은 처음부터 계약을 명시하는 모델입니다.

C++ templatesinstantiation 시에 요구식이 구체화template body를 작성concepts로 제약을 명시 가능Adabody는 계약 내에서 검사formal part에 계약을 작성instantiation에서 actual을 검사

Java나 C#의 제네릭은 참조형과 제약, 타입 소거, 실행 시 표현과의 관계가 설계의 중심이 됩니다. 반면 Ada의 제네릭은 컴파일 시점에 구체적인 인스턴스를 만드는 사고방식에 가깝습니다.

관점 Ada C++ Java Rust
계약 작성 방식 formal part에 타입・값・함수・패키지를 작성 templates / concepts 타입 파라미터와 bounds trait bounds
본체 검사 가인수의 계약 내에서 검사 인스턴스화 시의 구체화가 중심 bounds 내에서 검사 trait bounds 내에서 검사
실행 시 비용 정적 해결이 기본 정적 생성이 기본 타입 소거의 영향을 받음 단상화가 기본
값 파라미터 있음 있음 제한적 const generics
서브프로그램을 가인수로 삼기 있음 함수 객체 등으로 표현 람다/함수형 인터페이스 클로저/함수/trait
패키지를 가인수로 삼기 있음 템플릿 템플릿 등 없음 모듈 구조와는 별개

세부적인 언어 기능은 각기 다르지만, Ada의 특징은 ‘계약을 문법으로서 먼저 작성한다’는 점입니다.

14. 실무에서의 설계 판단 ── 무엇을 제네릭으로 만들어야 하는가

제네릭은 편리하지만, 무엇이든 제네릭으로 만들면 되는 것은 아닙니다. 실무에서는 다음과 같이 판단하면 실패하기 어려워집니다.

YesNoYesNoYesNoYesNo재사용하고 싶은 처리가 있다타입만 다른가?타입 파라미터를 검토크기나 임계값도 다른가?값 파라미터를 추가비교나 판정의 동작이 다른가?가인수 서브프로그램을 추가내부 상태나 API를 묶고 싶은가?총칭 패키지일반 서브프로그램으로 충분

14.1 총칭 서브프로그램이 적합한 상황

총칭 서브프로그램은 상태를 갖지 않는 알고리즘에 적합합니다.

  • Swap
  • Sort
  • Count_If
  • Find
  • Map 성격의 변환
  • Min / Max

알고리즘 본체가 짧고 입력과 출력이 명확한 경우에는, 패키지보다 서브프로그램 쪽이 읽기 쉬워집니다.

14.2 총칭 패키지가 적합한 상황

총칭 패키지는 타입과 함께 여러 연산이나 내부 상태를 가지고 싶은 경우에 적합합니다.

  • 고정 길이 스택
  • 링 버퍼
  • 소규모 딕셔너리
  • 통계 처리 세트
  • 디바이스별 I/O 추상화
  • 단위계가 있는 수치형의 연산 세트

특히 Ada에서는 패키지 명세부가 공개 API, 패키지 본체가 구현이라는 형태로 분리되어 있기 때문에, 제네릭 패키지는 ‘타입 안전한 모듈 템플릿’으로 사용할 수 있습니다.

은폐package spec공개 API이용하는 쪽package body내부 구현generic formal part타입・값・함수의 계약

14.3 가인수는 적게 시작하기

가인수를 지나치게 늘리면 인스턴스화를 읽기 어려워집니다. 처음에는 최소한으로 시작하고, 교체하고 싶은 이유가 생기면 늘리는 것이 안전합니다.

-- 읽기 어려워지기 쉬운 예
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- 이름 붙은 연관으로 의도를 남긴다
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

Ada에서는 인스턴스화 시에 이름 붙은 연관을 사용할 수 있습니다. 제네릭은 설계상의 중요한 점이 인스턴스화에 나타나기 때문에, 실무 코드에서는 이름을 붙여 작성하는 쪽이 유지보수하기 쉬운 경우가 많습니다.

15. 자주 발생하는 실수

Ada의 제네릭은 강력하지만, 처음에 실수하기 쉬운 지점이 있습니다.

15.1 private 타입으로는 대소 비교를 할 수 없다

다음과 같은 본체는 작성할 수 없습니다.

generic
   type Element is private;
function Bad_Min (A, B : Element) return Element;

function Bad_Min (A, B : Element) return Element is
begin
   if A < B then      -- 여기서 오류
      return A;
   else
      return B;
   end if;
end Bad_Min;

Elementprivate로만 선언되어 있기 때문에 <를 사용할 수 있다고 단정할 수 없습니다. 비교하고 싶다면 다음과 같이 계약에 추가합니다.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
본체에서 비교를 사용하고 싶다formal part에 비교 함수를 작성인스턴스화 시에 비교 가능성을 확인private뿐제네릭 본체에서 컴파일 오류

15.2 is <>는 ‘무엇이든 자동 추론’이 아니다

is <>는 편리하지만 마법은 아닙니다. 인스턴스화 지점에서 적합한 연산자나 서브프로그램이 가시적이어야 합니다. 독자적인 비교 함수를 다른 패키지에 둔 경우에는 적절하게 withuse를 하거나, 이름을 붙여 명시적으로 전달하는 것이 안전합니다.

procedure Sort_By_Age is new Generic_Insertion_Sort
  (Item_Type  => Person,
   Index      => Positive,
   Item_Array => Person_Array,
   "<"        => Younger_Than);

15.3 인스턴스마다 예외도 별개가 된다

제네릭 패키지의 명세부에 예외를 선언하면, 인스턴스마다 별개의 예외가 됩니다.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);

이 경우 Int_Stack.Stack_OverflowFloat_Stack.Stack_Overflow는 서로 다른 예외로 취급됩니다. 공통 예외로 다루고 싶다면 제네릭 바깥쪽에 예외를 정의하는 설계도 검토합니다.

다른 예외Generic_StackStack_Overflow 선언Int_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 코드 크기가 늘어날 수 있다

제네릭은 실행 시의 불필요한 간접 참조를 피하기 쉬운 한편, 타입마다 인스턴스를 만들기 때문에 인스턴스 수가 많으면 코드 크기가 늘어날 가능성이 있습니다.

이는 C++의 템플릿이나 Rust의 단상화에서도 볼 수 있는 트레이드오프입니다. 고신뢰성・임베디드・리얼타임 성향의 개발에서는 실행 시의 불확실성을 줄이는 대신 빌드 시 생성물 크기를 관리한다는 사고방식이 됩니다.

하나의 generic 본체Integer 버전Float 버전Long_Float 버전My_Type 버전생성된 코드실행 시 타입 판정이나 박싱을 피하기 쉬움인스턴스가 많으면 크기 증가에 주의

15.5 limited private를 사용해야 하는 상황

type Element is private;는 대입을 전제합니다. 파일 핸들, 잠금, 디바이스 핸들처럼 복사되어서는 안 되는 것을 다루는 경우에는 limited private를 검토합니다.

generic
   type Resource is limited private;
   with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);

복사할 수 없는 타입을 다루는 설계에서는, 값을 저장하는 컨테이너보다 절차를 적용하는 알고리즘이나 참조를 명시하는 설계 쪽이 더 안전합니다.

16. 작은 설계 패턴 모음

여기서부터는 실무에서 자주 사용하는 형태를 짧게 정리합니다.

16.1 비교 가능한 값에만 Min을 제공하기

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;

function Generic_Min (A, B : Element) return Element is
begin
   if A < B then
      return A;
   else
      return B;
   end if;
end Generic_Min;
Element비교 함수가 필요Generic_Min더 작은 쪽을 반환

16.2 임계값을 값 파라미터로 삼기

generic
   type Count_Type is range <>;
   Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;

function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
   return Value > Threshold;
end Generic_Is_Over;

값 파라미터는 실행 시의 설정값이 아니라, 인스턴스의 성질로서 고정하고 싶은 값에 적합합니다.

16.3 출력 수단을 주입하기

generic
   type Element is private;
   with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);

procedure Generic_Print_Twice (Item : Element) is
begin
   Put (Item);
   Put (Item);
end Generic_Print_Twice;

이런 형태로 만들면 표준 출력, 로그, 테스트용 버퍼 등 출력 대상을 교체할 수 있습니다.

Generic_Print_TwicePut을 가인수 서브프로그램으로 받는다콘솔 출력로그 출력테스트용 버퍼

16.4 배열의 첨자형을 고정하지 않기

Ada에서는 배열의 첨자형도 중요한 타입 정보입니다. Positive로 고정하지 않고, 필요에 따라 첨자형도 가인수로 삼으면 재사용성이 높아집니다.

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);

이런 설계라면 Positive 첨자 배열뿐 아니라 열거형 첨자 배열에도 대응할 수 있습니다.

Index is discretePositive rangeDay 열거형State 열거형독자적인 정수형

17. Ada다운 API로 만들기 위한 체크리스트

제네릭을 작성할 때는 마지막으로 다음 관점에서 다시 살펴보면 읽기 쉬워집니다.

Generic 설계 체크가인수는 최소한인가필요한 연산을 formal part에 명시했는가private / range / digits 등의 카테고리는 적절한가이름 붙은 연관으로 읽기 쉽게 인스턴스화할 수 있는가인스턴스별 상태나 예외를 의식하고 있는가코드 크기 증가를 허용할 수 있는가테스트용 인스턴스를 준비했는가

글로 정리하면 다음과 같습니다.

  • 본체에서 사용하는 연산은 반드시 가인수의 계약으로 보이는 형태로 만든다.
  • private로 충분하다면 private로 한다. 산술이 필요하면 range <>digits <>를 사용한다.
  • 비교, 해시, 출력, 변환 등 타입마다 달라지는 동작은 가인수 서브프로그램으로 만든다.
  • 크기나 임계값이 인스턴스의 성질이라면 값 파라미터로 만든다.
  • 상태를 가진다면 총칭 패키지, 상태를 갖지 않는다면 총칭 서브프로그램을 먼저 고려한다.
  • 인스턴스화에서는 인수가 많을수록 이름 붙은 연관을 사용한다.
  • 예외나 내부 상태는 인스턴스마다 독립한다는 것을 전제로 설계한다.

18. 예제 전체 구성 예시

글 속의 예제를 파일로 나눈다면, 다음과 같은 구성으로 만들면 읽기 쉬워집니다.

ada-generic-programmingsrcgenericsdemosgeneric_swap.adsgeneric_swap.adbgeneric_stack.adsgeneric_stack.adbgeneric_insertion_sort.adsgeneric_insertion_sort.adbgeneric_statistics.adsgeneric_statistics.adbgeneric_count_if.adsgeneric_count_if.adbgeneric_kv_store.adsgeneric_kv_store.adbswap_demo.adbstack_demo.adbsort_demo.adbstatistics_demo.adbcount_if_demo.adbkv_demo.adb

작은 기사용 예제라면 하나의 파일로 모아 gnatchop하는 것도 편리하지만, 실무나 장기 유지보수를 고려한다면 명세부 .ads와 본체 .adb를 나누는 쪽이 Ada다운 구성이 됩니다.

19. 정리 ── 타입으로 재사용의 경계를 정하기

Ada의 제네릭 프로그래밍은 ‘타입에 의존하지 않는 코드를 작성하는 기능’만이 아닙니다. 오히려 본질은 재사용 가능한 부품이 무엇을 요구하는지를 타입과 서브프로그램과 값의 계약으로 명시하는 것에 있습니다.

계약을 작성제네릭 본체를 작성타입・값・함수를 전달해 인스턴스화타입 안전하게 사용복사 없이 재사용

이 글에서 살펴본 것처럼, Ada의 제네릭에서는 다음과 같은 것들을 가인수로 만들 수 있습니다.

  • 타입
  • 서브프로그램
  • 패키지

나아가 타입에 대해서는 private, limited private, range <>, mod <>, digits <>, delta <>, (<>) 등 상당히 세밀한 카테고리를 지정할 수 있습니다. 이를 통해 제네릭 본체는 ‘가능한지 알 수 없는 연산’에 의존하지 않고, 계약에 적힌 연산만으로 안전하게 구현할 수 있습니다.

C나 오래된 C++ 자산에서는 재사용을 위해 매크로, void*, 함수 포인터, 손으로 작성한 타입 분기가 사용되는 경우가 있습니다. Ada의 제네릭은 그러한 용도의 많은 부분을 타입 안전하고 읽기 쉬운 형태로 대체할 수 있습니다. 특히 장기 유지보수, 임베디드, 리얼타임, 고신뢰성 소프트웨어에서는 이러한 ‘컴파일 시점에 경계를 정하는 설계’가 큰 가치를 지닙니다.

20. 관련 상담 영역

합동회사 코무라소프트에서는 Windows 애플리케이션 개발, 기존 자산의 조사・개수, COM / ActiveX / 32bit / 64bit 경계 정리, 기술 상담・설계 리뷰 등을 다루고 있습니다. Ada와 같은 정적 타이핑・고신뢰성 지향 설계뿐 아니라, 기존의 C/C++, C#, VB6, MFC, COM 자산을 어떻게 정리해 연명・이행할 것인가도 실무상 가까운 주제입니다.

참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Ada의 제네릭이란 무엇인가요?
타입, 값, 서브프로그램, 그리고 패키지 자체를 가인수(형식 파라미터)로 받아들여, new에 의한 인스턴스화 시점에 정적으로 타입 검사되는 재사용 메커니즘입니다. 단순한 텍스트 치환이 아니라, 컴파일 시점에 '이 부품이 이 계약을 만족하는가'를 확정합니다. 총칭 서브프로그램은 선언만으로는 호출할 수 없으며, 구체적인 타입을 지정해 인스턴스화해야 비로소 일반적인 프로시저나 함수로 사용할 수 있습니다.
Ada의 제네릭은 C++의 템플릿과 무엇이 다른가요?
Ada는 처음부터 계약을 명시하는 contract model을 채택하고 있어, 제네릭 본체는 가인수가 약속한 연산만을 사용해 작성되며 본체 단독으로 타입 검사가 이루어집니다. C++ 템플릿은 역사적으로 인스턴스화해야 비로소 오류가 드러나는 성질이 있었고, C++20의 concepts로 개선되었습니다. 또한 Ada에서는 값 파라미터, 서브프로그램 파라미터뿐 아니라 패키지 자체도 가인수로 받을 수 있어, 제네릭 부품들을 서로 조합할 수 있습니다.
Ada의 가인수 타입 파라미터에서 타입 카테고리를 지정하는 이유는 무엇인가요?
본체에서 사용할 수 있는 연산을 계약으로 명시하기 위해서입니다. type T is private로는 대입이나 동등 비교 같은 기본 연산만 전제할 수 있고, 대소 비교나 산술 연산은 사용할 수 없습니다. 정수 연산이 필요하면 range <>, 부동소수점 연산이 필요하면 digits <>, 비트 연산이 필요하면 mod <>처럼 카테고리를 지정합니다. 카테고리 지정은 읽는 사람에게 '이 부품이 무엇을 요구하는가'를 전달하는 타입 수준의 명세서로도 기능합니다.
Ada의 제네릭에서 주의해야 할 점이 있나요?
타입마다 인스턴스를 만들기 때문에 인스턴스 수가 많으면 코드 크기가 늘어날 수 있습니다. 이는 C++ 템플릿이나 Rust의 단상화(monomorphization)에서도 볼 수 있는 트레이드오프입니다. 또한 제네릭 패키지의 명세부에 선언한 예외는 인스턴스마다 별개의 예외가 됩니다. 가인수는 처음에는 최소한으로 하고, 교체하고 싶은 이유가 생기면 늘리는 것, 그리고 인수가 많은 인스턴스화에서는 이름 붙은 연관(named association)을 사용하는 것이 실무상의 지침입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기