레코드 심화

레코드 심화 (More about records)

이 장에서는 레코드의 고급 속성을 살펴볼게요. 특히 동적 크기 레코드, 판별자(discriminant), 변형 레코드(variant record)를 다뤄요.

출처: 레코드 심화 문서

본문

동적 크기 레코드 타입 (Dynamically sized record types)

앞서 레코드 타입의 간단한 예시를 봤어요. 이제 이 기본 언어 기능의 고급 속성을 살펴볼게요.

주목할 점은 레코드 타입의 객체 크기를 컴파일 타임에 알 필요가 없다는 것이에요. 아래 예시에서 볼 수 있죠:

package Runtime_Length is
   function Compute_Max_Len return Natural;
end Runtime_Length;

with Runtime_Length; use Runtime_Length;

package Var_Size_Record is
    Max_Len : constant Natural :=
                Compute_Max_Len;
    --          ^ Not known at compile time

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

    type Growable_Stack is record
       Items : Items_Array (1 .. Max_Len);
       Len   : Natural;
    end record;
    --  Growable_Stack is a definite type, but
    --  size is not known at compile time.

    G : Growable_Stack;
end Var_Size_Record;

레코드의 크기를 런타임에 결정하는 것은 완전히 괜찮아요. 다만 이 타입의 모든 객체는 같은 크기를 가진다는 점에 주의하세요.

판별자가 있는 레코드 (Records with discriminant)

위 예시에서 Items 필드의 크기는 런타임에 한 번 결정되지만, 모든 Growable_Stack 인스턴스는 정확히 같은 크기예요. 하지만 그게 원하는 것일 수도 있겠죠. 배열은 일반적으로 이 유연성을 제공한다는 걸 봤어요. 무제한 배열 타입에서는 서로 다른 객체가 다른 크기를 가질 수 있어요.

레코드에서도 판별자(discriminant) 라고 하는 특별한 종류의 필드를 사용해 유사한 기능을 얻을 수 있어요:

package Var_Size_Record_2 is
    type Items_Array is
      array (Positive range <>) of Integer;

    type Growable_Stack (Max_Len : Natural) is
    record
    --                   ^ Discriminant. Cannot be
    --                     modified once
    --                     initialized.
       Items : Items_Array (1 .. Max_Len);
       Len   : Natural := 0;
    end record;
    --  Growable_Stack is an indefinite type
    --  (like an array)
end Var_Size_Record_2;

판별자는 단순한 형태에서는 상수예요. 객체를 초기화하고 나면 수정할 수 없어요. 이것은 객체의 크기를 결정하므로 직관적으로 말이 돼요.

또한 판별자는 타입을 무한(indefinite)하게 만들어요. 판별자가 객체 크기 지정에 사용되든 아니든, 판별자가 초기화 없이 선언되면 판별자가 있는 타입은 무한이 돼요:

package Test_Discriminants is
   type Point (X, Y : Natural) is record
      null;
   end record;

   P : Point;
   --  ERROR: Point is indefinite, so you
   --  need to specify the discriminants
   --  or give a default value

   P2 : Point (1, 2);
   P3 : Point := (1, 2);
   --  Those two declarations are equivalent.

end Test_Discriminants;

이것은 또한 위 예시에서 Point 값의 배열을 선언할 수 없다는 뜻이에요. Point의 크기를 알 수 없기 때문이에요.

위 예시에서 언급했듯 판별자에 기본값을 제공할 수 있어요. 그러면 판별자를 지정하지 않고도 Point 값을 합법적으로 선언할 수 있어요. 위 예시에서는 이렇게 보일 거예요:

package Test_Discriminants is
   type Point (X, Y : Natural := 0) is record
      null;
   end record;

   P : Point;
   --  We can now simply declare a "Point"
   --  without further ado. In this case,
   --  we're using the default values (0)
   --  for X and Y.

   P2 : Point (1, 2);
   P3 : Point := (1, 2);
   --  We can still specify discriminants.

end Test_Discriminants;

Point 타입에 이제 기본 판별자가 있어도, P2P3 선언에서처럼 여전히 판별자를 지정할 수 있다는 점에 주의하세요.

대부분의 다른 측면에서 판별자는 일반 필드처럼 동작해요. 위에서 본 것처럼 애그리게이트에서 값을 지정해야 하고, 점 표기법으로 값에 접근할 수 있어요.

with Ada.Text_IO;       use Ada.Text_IO;

with Var_Size_Record_2; use Var_Size_Record_2;

procedure Main is
   procedure Print_Stack (G : Growable_Stack) is
   begin
      Put ("<Stack, items: [");
      for I in G.Items'Range loop
         exit when I > G.Len;
         Put (" " & Integer'Image (G.Items (I)));
      end loop;
      Put_Line ("]>");
   end Print_Stack;

   S : Growable_Stack :=
     (Max_Len => 128,
      Items   => (1, 2, 3, 4, others => <>),
      Len     => 4);
begin
   Print_Stack (S);
end Main;

참고: 위 예시에서는 배열의 크기를 결정하는 데 판별자를 사용했지만, 그것으로 제한되진 않아요. 예를 들어 중첩된 판별자 레코드의 크기를 결정하는 데도 쓸 수 있어요.

변형 레코드 (Variant records)

지금까지의 판별자 예시는 크기가 판별자에 달려 있는 컴포넌트를 가짐으로써 다양한 크기의 레코드를 선언하는 것을 보여줬어요. 하지만 판별자는 때로 "변형 레코드"라고 하는 기능, 즉 다른 필드 집합을 포함할 수 있는 레코드를 얻는 데도 사용될 수 있어요.

package Variant_Record is
   --  Forward declaration of Expr
   type Expr;

   --  Access to a Expr
   type Expr_Access is access Expr;

   type Expr_Kind_Type is (Bin_Op_Plus,
                           Bin_Op_Minus,
                           Num);
   --  A regular enumeration type

   type Expr (Kind : Expr_Kind_Type) is record
      --      ^ The discriminant is an
      --        enumeration value
      case Kind is
         when Bin_Op_Plus | Bin_Op_Minus =>
            Left, Right : Expr_Access;
         when Num =>
            Val : Integer;
      end case;
      --  Variant part. Only one, at the end of
      --  the record definition, but can be
      --  nested
   end record;
end Variant_Record;

when 분기에 있는 필드는 판별자 값이 그 분기에 포함될 때만 사용할 수 있어요. 위 예시에서 KindBin_Op_PlusBin_Op_Minus일 때만 LeftRight 필드에 접근할 수 있어요.

레코드에 유효하지 않은 필드에 접근하려 하면 Constraint_Error가 발생해요.

with Variant_Record; use Variant_Record;

procedure Main is
   E : Expr := (Num, 12);
begin
   E.Left := new Expr'(Num, 15);
   --  Will compile but fail at runtime
end Main;

표현식의 평가기(evaluator)는 이렇게 쓸 수 있어요:

with Ada.Text_IO;    use Ada.Text_IO;

with Variant_Record; use Variant_Record;

procedure Main is
   function Eval_Expr (E : Expr) return Integer is
     (case E.Kind is
      when Bin_Op_Plus  =>
             Eval_Expr (E.Left.all)
             + Eval_Expr (E.Right.all),
      when Bin_Op_Minus =>
             Eval_Expr (E.Left.all)
             - Eval_Expr (E.Right.all),
      when Num => E.Val);

   E : Expr := (Bin_Op_Plus,
                new Expr'(Bin_Op_Minus,
                          new Expr'(Num, 12),
                          new Expr'(Num, 15)),
                new Expr'(Num, 3));
begin
   Put_Line (Integer'Image (Eval_Expr (E)));
end Main;

Ada의 변형 레코드는 OCaml이나 Haskell 같은 함수형 언어의 합 타입(Sum types)과 매우 비슷해요. 주요 차이는 판별자가 Ada에서 별도 필드라는 점이에요. 합 타입의 '태그'는 일종의 내장돼 있고 패턴 매칭으로만 접근 가능하죠. 다른 차이도 있어요(Ada의 변형 레코드에는 여러 판별자를 가질 수 있어요). 그럼에도 불구하고 함수형 언어의 합 타입과 같은 종류의 타입 모델링을 가능하게 해요.

C/C++의 유니온과 비교하면, Ada 변형 레코드는 허용하는 것이 더 강력하고 런타임에 검사되어 더 안전해요.

더 알아보기 (Learn more)