레코드 심화
레코드 심화 (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 타입에 이제 기본 판별자가 있어도, P2와 P3 선언에서처럼 여전히 판별자를 지정할 수 있다는 점에 주의하세요.
대부분의 다른 측면에서 판별자는 일반 필드처럼 동작해요. 위에서 본 것처럼 애그리게이트에서 값을 지정해야 하고, 점 표기법으로 값에 접근할 수 있어요.
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 분기에 있는 필드는 판별자 값이 그 분기에 포함될 때만 사용할 수 있어요. 위 예시에서 Kind가 Bin_Op_Plus나 Bin_Op_Minus일 때만 Left와 Right 필드에 접근할 수 있어요.
레코드에 유효하지 않은 필드에 접근하려 하면 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 변형 레코드는 허용하는 것이 더 강력하고 런타임에 검사되어 더 안전해요.