Ada의 generic programming ── 타입으로 계약을 쓰고, 재사용을 제로 비용으로 구현한다

· 업데이트: · · Ada, ProgrammingLanguage, Generics, TypeSystem, StaticTyping, ContractModel, ZeroCostAbstraction, GNAT, Alire, 고신뢰성, 코드 재사용

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
글 서두에 "이 글의 지식 맵" 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대응해 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하면 됩니다.
대상 독자와 목적별 읽기 가이드를 서두에 추가하고, generic 단위 관련 용어 사전을 넣었습니다. 더불어 각 장의 코드 예에 실행 결과를 붙이고, 키와 값 데모에서 분기의 본문이 비어 있던 부분을 보완했습니다.
그림에서 Ada의 `<>`를 `box`로 표기하는 이유(Mermaid가 `<>`를 그리지 못하기 때문)를 각 그림 바로 뒤에 주석으로 달았습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635334)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「Ada의 generic programming ── 타입으로 계약을 쓰고, 재사용을 제로 비용으로 구현한다」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/ada-generic-programming/

DOI(등록된 아카이브)
10.5281/zenodo.21635334
DOI(마지막 등록 버전)
10.5281/zenodo.21635335

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

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

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

Ada의 generic은 단순한 텍스트 치환이 아닙니다. 타입, 값, subprogram, 나아가 package 자체를 formal parameter로 받아, instantiation 시점에 정적으로 타입 검사됩니다. 즉 실행 시점에 “이 타입으로 정말 괜찮은가”를 조사하는 것이 아니라, 컴파일 시점에 “이 부품이 이 계약을 충족하는가”를 확정하는 메커니즘입니다.

재사용하고 싶은 처리어떻게 재사용할 것인가복사와 붙여넣기void* / Object / 캐스트Ada generic수정 누락이 일어나기 쉽다런타임 오류나 타입 붕괴가 일어나기 쉽다타입 안전컴파일 시점 검사런타임의 여분 dispatch 없음

이 글에서는 Ada의 generic programming을 다음 흐름으로 정리합니다.

  • generic subprogram
  • generic package
  • 타입 parameter, 값 parameter, subprogram parameter
  • private, range <>, digits <> 등의 타입 카테고리
  • 정렬, 스택, 통계 처리, Count_If, 키-값 저장소의 구현 예
  • formal package parameter에 의한 고차 generic
  • Ada의 contract model과 실무 설계의 생각

1.1 대상 독자와, 이 글에서 가져갈 수 있는 것

이 글은 다음과 같은 분을 대상으로 합니다.

  • C++ template, C#이나 Java의 generics, Rust generics 중 하나를 써 본 경험이 있는 분
  • Ada 문법에는 익숙하지 않지만, “타입으로 계약을 쓴다”는 설계 생각에 관심이 있는 분
  • 장기 유지보수, 임베디드, 고신뢰성 분야에서 재사용 부품의 설계 방침을 검토하는 분

Ada 구문으로는 package(specification .ads와 body .adb의 분리)와 in / out / in out parameter mode를 알고 있으면 읽어 나갈 수 있습니다. 이 두 가지가 불안하면 먼저 “Ada 언어의 매력“을 보면 이해가 빨라집니다.

가져갈 것은 Ada 구문 그 자체보다, 재사용 가능한 부품이 무엇을 요구하는지를, 구현보다 먼저 타입으로 적어 낸다는 설계 습관입니다. 이 생각은 C#의 interface 제약이나 C++20 concepts를 어디까지 쓸지를 정할 때도 그대로 응용할 수 있습니다.

1.2 읽기 가이드 ── 전부를 읽을 필요는 없습니다

이 글은 20장입니다. 통독하지 않아도, 목적에 따라 다음과 같이 골라 읽을 수 있습니다.

목적 읽을 장
생각만 짧은 시간에 잡고 싶다 4장(기본 모델)→ 6장(최소 generic subprogram)→ 13장(contract model)
직접 부품을 쓰고 싶다 4장 → 6장 → 7장(generic package)→ 8장(동작 주입)→ 9장(타입 카테고리)
설계 판단의 지침만 필요하다 13장 → 14장(무엇을 generic으로 할 것인가)→ 15장(막히는 지점)→ 17장(체크리스트)
손을 움직여 보고 싶다 3장에서 환경을 만들고, 6장·7장의 완성된 샘플부터

가장 짧은 경로는 4장·6장·13장의 3장입니다. 이 3장에서 formal parameter, instantiation, contract model이라는 핵심이 갖춰집니다. 5장·9장은 formal parameter 목록이므로, 필요할 때 사전처럼 찾아 읽어도 됩니다.

이 주제는 이 블로그 연재 “Ada 언어의 매력”, “SPARK 형식 검증 입문”, “안전한 병행 처리”, “실시간 시스템“에 이어지는 위치입니다. Ada의 “타입으로 설계를 말한다”는 생각을 generic이라는 각도에서 파고듭니다.

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 이 글의 지도

먼저 전체 그림을 도식으로 잡습니다. Ada의 generic을 “타입을 인수로 받는 기능”으로만 이해하면 꽤 좁은 시야가 됩니다. 실제로는 재사용하고 싶은 단위에 따라 subprogram, package, subprogram parameter, 값 parameter, formal package parameter를 조합합니다.

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

그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.

이 글의 읽기 방법은 단순합니다. 전반에서는 구문을 따라가고, 후반에서는 설계 판단을 다룹니다. Ada를 처음 읽는 분은 처음부터 세세한 구문을 외우려 하지 말고, “무엇을 formal parameter로 두었는가”, “그 formal parameter에 어떤 연산을 허용하는가”에 주목하면 됩니다.

2.1 용어 미니 사전

이후 반복해서 나오는 말을 먼저 한영으로 맞춰 둡니다. Ada의 일본어 문헌에서는 generic을 “総称”으로 옮기는 경우가 많고, 이 글에서도 같은 개념을 가리키지만, 한국어 기술 문맥에 맞춰 “generic subprogram”, “generic package”로 씁니다. 의미는 “generic한” 것과 같습니다.

이 글의 표기 영어 의미
generic 단위 generic unit generic으로 시작하는 선언. generic subprogram과 generic package를 묶은 이름
formal parameter generic formal parameter generic과 선언 본문 사이에 쓰는, 받는 쪽의 인수. 타입·값·subprogram·package 중 하나
formal part generic formal part formal parameter를 나열한 부분 그 자체. “계약을 쓰는 장소”로 읽어도 됩니다
actual parameter generic actual parameter instantiation 때 실제로 넘기는 타입·값·subprogram·package
instantiation instantiation new로 generic 단위에서 일반 subprogram이나 package를 만드는 것
box <> Ada에서 <> 기호의 이름. range <>처럼 “구체적인 타입은 instantiation 때 정한다”는 뜻
contract model contract model formal parameter에 적은 약속의 범위만으로 본문을 쓰고, 본문만으로 타입 검사하는 Ada 방식(13장)

본문에서는 Ada 표기 그대로 range <>, digits <>로 쓰지만, 그림 안에서만 range box, digits box로 씁니다. Mermaid는 <>를 포함한 레이블을 그대로 그리지 못하기 때문이며, 의미는 본문과 같습니다. 위 표대로 Ada 자신이 <>를 box라고 부르므로, 그림 표기도 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

여러 컴파일 단위를 1파일에 모은 샘플은 gnatchop으로 나눈 뒤 gnatmake합니다.

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

-gnata는 assertion을 켜는 옵션입니다. generic 자체를 쓰기 위해 필수는 아니지만, 학습용 샘플에서는 계약이나 경계 조건을 확인하기 쉬워집니다.

실행 파일gnatmakegnatchop개발자실행 파일gnatmakegnatchop개발자하나의 .ada 파일을 넘긴다.ads / .adb / main으로 분할gnatmake -gnata mainbind와 link까지 실행./main실행 결과

4. Ada generic의 기본 모델

Ada의 generic은 대략 다음 3가지로 생각하면 이해하기 쉽습니다.

  1. generic 단위를 쓴다
  2. generic 부분에 formal parameter를 쓴다
  3. 쓰는 쪽에서 new로 instantiation한다
generic 선언formal parametergeneric 본문new에 의한 instantiation일반 subprogram 또는 package로 사용타입값subprogrampackage

예를 들어 두 값을 맞바꾸는 처리를 generic으로 만들면, 타입만 formal parameter로 둘 수 있습니다.

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

이 시점에서는 Generic_Swap을 아직 호출할 수 없습니다. 이것은 “임의의 Element 타입에 대해 쓸 수 있는 맞바꾸기 처리의 template”입니다. 구체적인 타입을 주어야 비로소 일반 procedure가 됩니다.

procedure Swap_Integer is new Generic_Swap (Integer);

그림으로 보면 다음과 같은 관계입니다.

Integer를 넘긴다Character를 넘긴다My_Record를 넘긴다Generic_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordInteger 변수를 맞바꾼다Character 변수를 맞바꾼다My_Record 변수를 맞바꾼다

중요한 것은 template 본문이 Element에 대해 쓸 수 있는 연산만으로 쓰여 있다는 점입니다. type Element is private;로 선언한 경우, 대입이나 동등 비교 같은 기본 연산은 쓸 수 있지만 대소 비교나 산술 연산은 쓸 수 없습니다. 즉 generic 선언 자체가 “이 부품은 무엇을 전제로 해도 되는가”를 나타냅니다.

5. formal parameter의 종류 ── Ada generic의 어휘

Ada generic에서 받을 수 있는 것은 타입만이 아닙니다. 여기가 C#이나 Java의 일반적인 generics와 크게 다른 점입니다.

generic formal parameters타입 parameter객체 / 값 parametersubprogram parameterpackage parametertype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...with package P is new ...

그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.

대표적인 formal parameter를 표로 보면 다음과 같습니다.

종류 예 의미
타입 parameter type Element is private; 임의의 definite한 non-limited 타입을 받는 기본형
limited 타입 parameter type Element is limited private; 복사할 수 없는 타입도 받는다
이산형 type Index is (<>); 정수형이나 열거형 등, 배열 첨자로 쓸 수 있는 타입
부호 있는 정수형 type Count is range <>; +, -, 대소 비교 등 정수 연산을 전제할 수 있다
modular 정수형 type Word is mod <>; 비트 연산이나 나머지 기반 정수를 다룬다
부동소수점형 type Real is digits <>; Float, Long_Float, 사용자 정의 부동소수점형 등
고정소수점형 type Money is delta <>; 고정소수점 연산을 다룬다
값 parameter Max_Size : Positive; 크기나 임계값 등을 instance마다 고정한다
subprogram with function Predicate (...) return Boolean; 비교 함수나 술어 등, 동작을 주입한다
package with package P is new Some_Generic (<>); 이미 만든 generic package를 부품으로 받는다

이 어휘가 있기 때문에 Ada에서는 “무엇이든 받지만 안에서 위험한 일을 한다”가 아니라 “이 연산을 할 수 있는 타입만 받는다”는 쓰기가 자연스럽게 됩니다.

6. generic subprogram ── Generic_Swap으로 최소 구성을 이해한다

첫 예로, 임의의 타입의 두 변수를 맞바꾸는 Generic_Swap을 봅니다.

6.1 specification

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

generic에 이어지는 부분이 formal parameter입니다. 여기서는 Element라는 타입을 받습니다. is private는 generic 본문에서 볼 때 그 타입의 내부 표현을 모른다는 뜻입니다.

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

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

6.2 body

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, record 타입, 열거형 등 대입 가능한 타입이면 자연스럽게 쓸 수 있습니다.

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

6.3 instantiation

쓰는 쪽에서는 new를 씁니다.

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

이제 Swap_Int와 Swap_Char는 일반 procedure로 호출할 수 있습니다.

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_Int는 Integer용으로 instantiation된 일반 procedure이기 때문입니다. generic은 “무엇이든 들어가는 구멍”이 아니라 “타입마다 안전한 구체물을 만드는 메커니즘”으로 보면 이해하기 쉽습니다.

7. generic package ── 타입과 값을 parameter로 둔다

subprogram 하나가 아니라, 여러 연산과 내부 상태를 묶어 재사용하고 싶을 때는 generic package를 씁니다. 대표 예는 스택입니다.

스택은 요소 타입과 최대 크기만 바꾸면, 기본 로직은 같습니다.

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 specification

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;

여기서는 두 종류의 formal parameter를 씁니다.

  • Element_Type은 타입 parameter
  • Max_Size는 값 parameter

Max_Size는 Positive이므로, 0 이하의 크기에서는 instantiation할 수 없습니다. 이렇게 값 parameter에도 타입에 의한 제약을 둘 수 있습니다.

7.2 body

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;

이 package body에서 중요한 것은, Data와 Top이 instance마다 따로 만들어진다는 점입니다.

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

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

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;

3장과 같은 절차로 빌드해 실행합니다.

gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1

= 오른쪽에 공백이 하나 들어가는 것은, 정수형의 'Image가 음이 아닌 값 앞에 공백을 한 글자 두는 사양 때문입니다. Push를 3번, Pop을 2번 했으므로 마지막 Size는 1이 됩니다. 스택이 가득 찬 상태(이 예에서는 Max_Size의 5개)에서 추가로 Push를 부르면 Int_Stack.Stack_Overflow가, 빈 상태에서 Pop을 부르면 Int_Stack.Stack_Underflow가 발생합니다.

generic package는 실무에서 “작은 컨테이너”, “고정 길이 버퍼”, “링 버퍼”, “로그용 큐”, “하드웨어 추상화 계층” 등에서 효과가 큽니다. 특히 Ada에서는 크기를 실행 시점에 바꾸는 것보다, 타입이나 값 parameter로 정적으로 고정하는 설계가 고신뢰 시스템과 잘 맞습니다.

8. formal subprogram parameter ── 동작을 주입한다

타입만 받아도 아직 표현하지 못하는 것이 있습니다. 예를 들어 정렬에서는 요소 타입뿐 아니라 “어느 쪽을 먼저 늘어놓을까”라는 비교 로직이 필요합니다.

Ada에서는 이 비교 함수를 generic의 formal parameter로 둘 수 있습니다.

Generic_Insertion_SortItem_TypeIndexItem_Array비교 함수표준 비교를 쓴다Greater를 넘겨 내림차순독자 순서를 넘긴다

8.1 specification

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개의 formal parameter가 있습니다.

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

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

with function "<" ... is <>;의 is <>는, actual parameter를 생략한 경우 보이는 표준 연산자나 적합한 함수를 쓴다는 뜻입니다. 즉 Integer처럼 이미 <가 있는 타입이면 비교 함수를 명시하지 않아도 쓸 수 있습니다.

8.2 body

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;

삽입 정렬은 큰 배열에는 맞지 않지만, generic 설명에는 맞습니다. 비교 함수만 갈아 끼우면 같은 루프 구조를 오름차순에도 내림차순에도 쓸 수 있기 때문입니다.

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++에서 비교 함수 객체를 template 인수로 넘기는 설계나, Rust에서 trait bound로 순서를 요구하는 설계에 가깝습니다. 다만 Ada에서는 formal subprogram parameter로 “이 형태의 함수를 넘긴다”고 명시합니다.

8.4 실행 예

18장 구성에 따라 Generic_Insertion_Sort를 별도 파일(generic_insertion_sort.ads / .adb)에 둔 경우의 호출 측은 다음과 같습니다.

with Ada.Text_IO;            use Ada.Text_IO;
with Generic_Insertion_Sort;

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

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

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

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

   procedure Show (Label : String; Items : Int_Array) is
   begin
      Put (Label);
      for V of Items loop
         Put (Integer'Image (V));
      end loop;
      New_Line;
   end Show;

   Asc  : Int_Array := (99, 3, 47, 12);
   Desc : Int_Array := (99, 3, 47, 12);
begin
   Sort_Asc (Asc);
   Sort_Desc (Desc);
   Show ("Asc :", Asc);
   Show ("Desc:", Desc);
end Sort_Demo;
gnatchop ../src/snippets/03_sort.ada
gnatmake -gnata sort_demo
./sort_demo
Asc : 3 12 47 99
Desc: 99 47 12 3

같은 Generic_Insertion_Sort 본문에서 만든 두 procedure가, 비교 함수만 갈아 끼워 역순이 된 것을 확인할 수 있습니다. 요소 앞의 공백은 정수형 'Image가 음이 아닌 값 앞에 공백을 한 글자 두기 때문입니다.

9. 타입 카테고리 ── private보다 구체적인 계약을 쓴다

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

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

그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.

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이 부동소수점형임을 알 수 있습니다. 따라서 generic 본문에서는 +, -, *, / 등을 쓸 수 있습니다.

9.2 body

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 Float와 Long_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 실행 예

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Float_Text_IO;  use Ada.Float_Text_IO;
with Generic_Statistics;

procedure Statistics_Demo is
   type Float_Array is array (Positive range <>) of Float;

   package Float_Stats is new Generic_Statistics (Float, Float_Array);

   Samples : constant Float_Array := (1.0, 2.0, 3.0, 4.0);
begin
   Put ("Mean     = ");
   Put (Float_Stats.Mean (Samples), Fore => 1, Aft => 3, Exp => 0);
   New_Line;

   Put ("Variance = ");
   Put (Float_Stats.Variance (Samples), Fore => 1, Aft => 3, Exp => 0);
   New_Line;
end Statistics_Demo;
gnatchop ../src/snippets/04_statistics.ada
gnatmake -gnata statistics_demo
./statistics_demo
Mean     = 2.500
Variance = 1.250

Ada.Float_Text_IO는 Ada.Text_IO.Float_IO를 Float에 대해 인스턴스화한 표준 라이브러리이며, Exp => 0을 지정하면 지수 형식이 아니라 보통의 소수 표기가 됩니다. Aft => 3이 소수부 자릿수입니다. 이 Variance는 표본분산이 아니라 모분산(Values'Length로 나누는 형태)이라는 점에 주의해야 합니다.

9.5 카테고리 지정은 “타입 수준의 명세서”

카테고리 지정은 컴파일러를 달래기 위한 구문만은 아닙니다. 읽는 사람에게도 “이 부품이 무엇을 요구하는가”를 전하는 명세서가 됩니다.

쓰고 싶은 처리 맞는 formal 타입 이유
맞바꾸기, 저장, 꺼내기 private 대입만 되면 된다
복사할 수 없는 리소스 관리 limited private 대입을 전제하지 않는다
배열 첨자, 열거 상태 순회 (<>) First, Last, Succ, Pred를 쓸 수 있다
정수 합계, 카운터 range <> 정수 연산을 전제할 수 있다
비트 마스크, 순환 카운터 mod <> modular 연산을 전제할 수 있다
평균, 분산, 수치 계산 digits <> 부동소수점 연산을 전제할 수 있다
금액, 제어량 등의 고정 정밀도 delta <> 고정소수점 연산을 전제할 수 있다

10. 술어 주입 ── Count_If를 Ada답게 쓴다

formal subprogram parameter는 비교 함수뿐 아니라 술어에도 쓸 수 있습니다. 술어란 값을 받아 Boolean을 반환하는 함수입니다.

C#의 Func<T, bool>, Java의 Predicate<T>, C++의 람다나 함수 객체에 가까운 역할을, Ada에서는 generic의 formal subprogram으로 표현할 수 있습니다.

10.1 specification

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;

여기서는 Predicate에 is <>를 붙이지 않았습니다. 표준으로 보이는 술어 함수가 있는 것은 아니므로, 쓰는 쪽이 반드시 넘기도록 한 설계입니다.

10.2 body

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에서 고차적인 설계의 기본형입니다.

10.4 실행 예

with Ada.Text_IO;       use Ada.Text_IO;
with Generic_Count_If;

procedure Count_If_Demo is
   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);

   Data : constant Int_Array := (12, 7, 88, 3, 56, 91, 44, 19, 62);
begin
   Put_Line ("Even  =" & Natural'Image (Count_Even (Data)));
   Put_Line ("Large =" & Natural'Image (Count_Large (Data)));
end Count_If_Demo;
gnatchop ../src/snippets/05_filter.ada
gnatmake -gnata count_if_demo
./count_if_demo
Even  = 5
Large = 4

Data의 9개 요소 중 짝수는 12, 88, 56, 44, 62의 5개, 50보다 큰 것은 88, 56, 91, 62의 4개입니다.

11. 여러 parameter의 합성 ── 범용 키-값 저장소

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

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

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

11.1 specification

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;

이 package에는 4개의 formal parameter가 있습니다.

parameter 종류 역할
Key_Type 타입 키의 타입
Value_Type 타입 값의 타입
"=" subprogram 키의 일치 판정
Max_Entries 값 최대 엔트리 수

Max_Entries에는 := 50으로 기본값을 줍니다. 따라서 특별히 지정하지 않으면 50건의 저장소가 됩니다.

11.2 body

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 instance호출 측Keys/Values/UsedGeneric_KV_Store instance호출 측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 instantiation 예

with Ada.Text_IO;           use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Generic_KV_Store;

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
      Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
   else
      Put_Line ("1 => (not found)");
   end if;

   --  기존 키에 대한 Put은 덮어쓰기가 된다
   Int_String_Store.Put (1, To_Unbounded_String ("Ada 2022"));
   Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));

   if Int_String_Store.Contains (9) then
      Put_Line ("9 => " & To_String (Int_String_Store.Get (9)));
   else
      Put_Line ("9 => (not found)");
   end if;
end KV_Demo;
gnatchop ../src/snippets/06_kv_store.ada
gnatmake -gnata kv_demo
./kv_demo
1 => Ada
1 => Ada 2022
9 => (not found)

Contains로 존재를 확인하지 않고 Get을 부르면, 키가 없을 때 Key_Not_Found가 발생합니다. 위 예처럼 Contains로 분기하거나, 예외 핸들러를 쓰거나 둘 중 하나를 고르면 됩니다.

"="는 생략했습니다. 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. formal package parameter ── generic을 더 부품화한다

Ada의 generic에서는 package 자체를 formal parameter로 둘 수 있습니다. 이를 쓰면 “어떤 generic package에서 만든 instance”를 다른 generic의 입력으로 다룰 수 있습니다.

Generic_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 스택을 받는 logger

예를 들어 앞의 Generic_Stack instance를 받아, 그 크기를 표시하는 logger를 만든다고 합시다.

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;

쓰는 쪽에서는 먼저 스택을 만들고, 그 스택을 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);

이런 설계로 generic 부품끼리 조합할 수 있습니다.

2단계1단계Int_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

C++의 template template parameter에 가까운 용도이지만, Ada에서는 “이 generic package의 instance를 받는다”고 명시할 수 있습니다. 대규모 Ada 코드에서는 컨테이너, 알고리즘, 로그, 검사, 테스트 보조 등을 나눠 조합할 때 편리합니다.

13. Contract Model ── Ada generic에서 가장 중요한 생각

Ada generic을 이해하는 데 중요한 것이 contract model입니다.

generic 본문은 formal parameter가 약속한 연산만 사용해 써야 합니다. 예를 들어 type Element is private;만 선언했는데 Element에 대해 <를 쓸 수는 없습니다. <를 쓰고 싶다면 formal subprogram으로 명시하거나, 타입 카테고리를 더 구체적으로 해야 합니다.

generic formal part계약generic body계약 범위 안에서 구현본문만으로 타입 검사instantiationactual parameters실타입·실함수·실값actual parameter가 계약을 충족하는지 검사일반 package/subprogram

이 설계로 generic의 이용자뿐 아니라, generic을 작성하는 쪽도 지켜집니다.

13.1 C++ template과의 차이

C++ template은 강력하지만, 역사적으로는 “template 본문을 instantiation해야만 오류가 나온다”는 성질이 있었습니다. C++20 concepts로 개선되었지만, Ada generic은 처음부터 계약을 명시하는 모델입니다.

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

Java나 C#의 generics는 참조형이나 제약, type erasure, 런타임 표현과의 관계가 설계의 중심이 됩니다. 한편 Ada generic은 컴파일 시점에 구체적인 instance를 만드는 생각에 가깝습니다.

관점 Ada C++ Java Rust
계약의 쓰기 formal part에 타입·값·함수·package를 쓴다 templates / concepts 타입 parameter와 bounds trait bounds
본문의 검사 formal parameter의 계약 안에서 검사 instantiation 때 구체화가 중심 bounds 안에서 검사 trait bounds 안에서 검사
런타임 비용 정적 해결이 기본 정적 생성이 기본 type erasure의 영향을 받는다 monomorphization이 기본
값 parameter 있다 있다 제한적 const generics
subprogram을 formal parameter로 있다 함수 객체 등으로 표현 람다/함수형 interface 클로저/함수/trait
package를 formal parameter로 있다 template template 등 없다 모듈 구조와는 별개

세세한 언어 기능은 각각 다르지만, Ada의 특징은 “계약을 구문으로 먼저 쓴다”는 점입니다.

14. 실무에서의 설계 판단 ── 무엇을 generic으로 해야 하는가

generic은 편리하지만, 무엇이든 generic으로 만들면 되는 것은 아닙니다. 실무에서는 다음과 같이 판단하면 실패하기 어렵습니다.

YesNoYesNoYesNoYesNo재사용하고 싶은 처리가 있다타입만 다른가?타입 parameter를 검토크기나 임계값도 다른가?값 parameter를 추가비교나 판정의 동작이 다른가?formal subprogram을 추가내부 상태나 API를 묶고 싶은가?generic package일반 subprogram으로 충분

14.1 generic subprogram이 맞는 장면

generic subprogram은 상태를 갖지 않는 알고리즘에 맞습니다.

  • Swap
  • Sort
  • Count_If
  • Find
  • Map적인 변환
  • Min / Max

알고리즘 본문이 짧고, 입력과 출력이 명확한 경우에는 package보다 subprogram이 읽기 쉽습니다.

14.2 generic package가 맞는 장면

generic package는 타입과 함께 여러 연산이나 내부 상태를 갖고 싶을 때 맞습니다.

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

특히 Ada에서는 package specification이 공개 API, package body가 구현이라는 분리이므로, generic package는 “타입 안전한 모듈 template”로 쓸 수 있습니다.

은닉package spec공개 API이용 측package body내부 구현generic formal part타입·값·함수의 계약

14.3 formal parameter는 적게 시작한다

formal parameter를 너무 늘리면 instantiation이 읽기 어려워집니다. 처음에는 최소한으로 두고, 갈아 끼울 이유가 생기면 늘리는 편이 안전합니다.

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

-- named association으로 의도를 남긴다
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

Ada에서는 instantiation 때 named association을 쓸 수 있습니다. generic은 설계상 중요한 점이 instantiation에 드러나므로, 실무 코드에서는 이름 붙여 쓰는 편이 유지보수하기 쉬운 경우가 많습니다.

15. 자주 막히는 지점

Ada generic은 강력하지만, 처음에 막히기 쉬운 점이 있습니다.

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;

Element는 private로만 선언되어 있으므로 <를 쓸 수 있다고는 할 수 없습니다. 비교하고 싶다면 다음과 같이 계약에 추가합니다.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
본문에서 비교를 쓰고 싶다formal part에 비교 함수를 쓴다instantiation 때 비교 가능성을 확인private만generic 본문에서 컴파일 오류

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

is <>는 편리하지만 마법은 아닙니다. instantiation 지점에서 적합한 연산자나 subprogram이 보여야 합니다. 독자 비교 함수를 다른 package에 둔 경우에는 적절히 with나 use를 하거나, 이름을 붙여 명시적으로 넘기는 편이 안전합니다.

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

15.3 instance마다 예외도 별개가 된다

generic package의 specification에 예외를 선언하면, instance마다 별개의 예외가 됩니다.

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

이 경우 Int_Stack.Stack_Overflow와 Float_Stack.Stack_Overflow는 별개의 예외로 다뤄집니다. 공통 예외로 다루고 싶다면, generic 바깥에 예외를 정의하는 설계도 검토합니다.

별개의 예외Generic_StackStack_Overflow 선언Int_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

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

generic은 런타임의 여분 간접 참조를 피하기 쉬운 한편, 타입마다 instance를 만들기 때문에 instance 수가 많으면 코드 크기가 늘어날 수 있습니다.

이는 C++ template이나 Rust의 monomorphization에서도 보이는 trade-off입니다. 고신뢰·임베디드·실시간 쪽 개발에서는 런타임의 불확실성을 줄이는 대신, 빌드 시점의 생성물 크기를 관리한다는 생각이 됩니다.

하나의 generic 본문Integer판Float판Long_Float판My_Type판생성 코드런타임의 타입 판정이나 boxing을 피하기 쉽다instance가 많으면 크기 증가에 주의

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 임계값을 값 parameter로 둔다

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;

값 parameter는 런타임 설정값이 아니라, instance의 성질로 고정하고 싶은 값에 맞습니다.

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을 formal subprogram으로 받는다콘솔 출력로그 출력테스트용 버퍼

16.4 배열의 첨자 타입을 고정하지 않는다

Ada에서는 배열의 첨자 타입도 중요한 타입 정보입니다. Positive로 고정하지 않고, 필요하면 첨자 타입도 formal parameter로 두면 재사용성이 올라갑니다.

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을 쓸 때는 마지막에 다음 관점으로 다시 보면 읽기 쉬워집니다.

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

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

  • 본문에서 쓰는 연산은 반드시 formal parameter의 계약으로 보이게 한다.
  • private로 충분하면 private로 둔다. 산술이 필요하면 range <>나 digits <>를 쓴다.
  • 비교, 해시, 출력, 변환처럼 타입마다 바뀌는 동작은 formal subprogram으로 둔다.
  • 크기나 임계값이 instance의 성질이면 값 parameter로 둔다.
  • 상태가 있으면 generic package, 상태가 없으면 generic subprogram을 먼저 생각한다.
  • instantiation에서는 인수가 많을수록 named association을 쓴다.
  • 예외와 내부 상태는 instance마다 독립한다는 전제로 설계한다.

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

작은 글용 샘플이라면 1파일에 모아 gnatchop하는 것도 편리하지만, 실무나 장기 유지보수를 생각하면 specification .ads와 body .adb를 나누는 편이 Ada다운 구성이 됩니다.

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

Ada의 generic programming은 “타입에 의존하지 않는 코드를 쓰는 기능”만은 아닙니다. 본질은 오히려 재사용 가능한 부품이 무엇을 요구하는지를, 타입과 subprogram과 값의 계약으로 명시하는 것에 있습니다.

계약을 쓴다generic 본문을 쓴다타입·값·함수를 넘겨 instantiation타입 안전하게 쓴다복사 없이 재사용

이 글에서 본 것처럼, Ada generic에서는 다음을 formal parameter로 둘 수 있습니다.

  • 타입
  • 값
  • subprogram
  • package

나아가 타입에 대해서는 private, limited private, range <>, mod <>, digits <>, delta <>, (<>) 등 꽤 세세한 카테고리를 지정할 수 있습니다. 이로써 generic 본문은 “할 수 있는지 모르는 연산”에 의존하지 않고, 계약에 적힌 연산만으로 안전하게 구현할 수 있습니다.

C나 오래된 C++ 자산에서는 재사용을 위해 매크로, void*, 함수 포인터, 손으로 쓴 타입 분기가 쓰이기도 합니다. Ada generic은 그런 용도의 상당수를 타입 안전하고 읽기 쉬운 형태로 바꿀 수 있습니다. 특히 장기 유지보수, 임베디드, 실시간, 고신뢰성 소프트웨어에서는 이런 “컴파일 시점에 경계를 정하는 설계”가 큰 가치를 갖습니다.

20. 관련된 상담 영역

合同会社小村ソフト에서는 Windows 앱 개발, 기존 자산의 조사·수정, COM / ActiveX / 32bit / 64bit의 경계 정리, 기술 상담·설계 리뷰 등을 다룹니다. Ada처럼 정적 타이핑·고신뢰성 쪽 설계뿐 아니라, 기존의 C/C++, C#, VB6, MFC, COM 자산을 어떻게 정리해 수명 연장·이전할지도 실무에서 가까운 주제가 됩니다.

참고 링크

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

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

자주 묻는 질문

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

Ada의 generic이란 무엇입니까?
타입, 값, subprogram, package 자체를 formal parameter로 받아, new로 instantiation하는 시점에 정적으로 타입 검사되는 재사용 메커니즘입니다. 단순한 텍스트 치환이 아니라, 컴파일 시점에 "이 부품이 이 계약을 충족하는가"를 확정합니다. generic subprogram은 선언만으로는 호출할 수 없고, 구체적인 타입을 주어 instantiation해야 비로소 일반 procedure나 function으로 쓸 수 있습니다.
Ada의 generic은 C++ template과 무엇이 다릅니까?
Ada는 처음부터 계약을 명시하는 contract model을 채택하며, generic 본문은 formal parameter가 약속한 연산만 사용해 작성되고, 본문만으로 타입 검사됩니다. C++ template은 역사적으로 instantiation해야만 오류가 드러나는 성질이 있었고, C++20 concepts로 개선되었습니다. 또한 Ada에서는 값 parameter, subprogram parameter에 더해 package 자체를 formal parameter로 둘 수 있어, generic 부품끼리 조합할 수 있습니다.
Ada의 formal 타입 parameter에 타입 카테고리를 지정하는 이유는 무엇입니까?
본문에서 쓸 수 있는 연산을 계약으로 명시하기 위해서입니다. type T is private에서는 대입이나 동등 비교 같은 기본 연산만 전제할 수 있고, 대소 비교나 산술 연산은 쓸 수 없습니다. 정수 연산이 필요하면 range <>, 부동소수점 연산이 필요하면 digits <>, 비트 연산이면 mod <>처럼 카테고리를 지정합니다. 카테고리 지정은 읽는 사람에게 "이 부품이 무엇을 요구하는가"를 전하는 타입 수준의 명세서로도 기능합니다.
Ada generic에서 주의할 점은 있습니까?
타입마다 instance를 만들기 때문에, instance 수가 많으면 코드 크기가 늘어날 수 있습니다. 이는 C++ template이나 Rust의 monomorphization에서도 보이는 trade-off입니다. 또한 generic package의 specification에 선언한 예외는 instance마다 별개의 예외가 됩니다. formal parameter는 처음에는 최소한으로 두고, 교체할 이유가 생기면 늘리며, 인수가 많은 instantiation에서는 named association을 쓰는 것이 실무 지침입니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기