서브프로그램
서브프로그램 (Subprograms)
지금까지 우리는 주로 실행할 코드 본문을 가지기 위해 프로시저를 사용했어요. 프로시저는 서브프로그램(subprogram) 의 한 종류예요. Ada에는 함수(function)와 프로시저(procedure) 두 종류의 서브프로그램이 있고, 함수는 값을 반환하고 프로시저는 반환하지 않는다는 점이 둘의 차이예요.
출처: 서브프로그램 문서
본문
함수의 선언과 정의
다음 예시는 함수의 선언과 정의를 보여줘요:
function Increment (I : Integer) return Integer;
-- We declare (but don't define) a function with
-- one parameter, returning an integer value
function Increment (I : Integer) return Integer is
-- We define the Increment function
begin
return I + 1;
end Increment;
Ada의 서브프로그램은 물론 매개변수를 가질 수 있어요. 구문상 중요한 점 하나는, 매개변수가 없는 서브프로그램은 매개변수 섹션이 아예 없다는 거예요. 예를 들어:
procedure Proc;
function Func return Integer;
앞선 예시의 또 다른 변형을 볼게요:
function Increment_By
(I : Integer := 0;
Incr : Integer := 1) return Integer;
-- ^ Default value for parameters
이 예시에서 매개변수가 기본값을 가질 수 있음을 볼 수 있어요. 서브프로그램을 호출할 때 기본값이 있는 매개변수는 생략할 수 있어요. C/C++와 달리, 매개변수가 없는 서브프로그램 호출은 괄호를 포함하지 않아요.
위 함수의 구현은 다음과 같아요:
function Increment_By
(I : Integer := 0;
Incr : Integer := 1) return Integer is
begin
return I + Incr;
end Increment_By;
GNAT 툴체인에서: Ada 표준은 서브프로그램의 선언(specification)이나 구현을 어떤 파일에 저장해야 하는지 강제하지 않아요. 즉 표준은 특정 파일 구조나 파일 확장자를 요구하지 않아요. 예를 들어 위 Increment 함수의 선언과 구현을 둘 다 increment.txt라는 파일에 저장해도 표준 관점에서는 완전히 받아들여져요(시스템 전체 소스 코드도 단일 파일에 저장할 수 있어요). 하지만 GNAT 툴체인은 다음 파일 명명 규칙을 요구해요:
.ads확장자 파일은 선언을,.adb확장자 파일은 구현을 담아요.
따라서 GNAT 툴체인에서 Increment 함수의 선언은 increment.ads에, 구현은 increment.adb에 저장해야 해요. 이 규칙은 나중에 다룰 패키지에 항상 적용돼요(물론 이 규칙을 우회할 수는 있어요). 자세한 내용은 GNAT 툴체인 소개 과정이나 GPRbuild 사용자 가이드를 참고하세요.
서브프로그램 호출 (Subprogram calls)
이제 우리 서브프로그램을 이런 식으로 호출할 수 있어요:
with Ada.Text_IO; use Ada.Text_IO;
with Increment_By;
procedure Show_Increment is
A, B, C : Integer;
begin
C := Increment_By;
-- ^ Parameterless call,
-- value of I is 0
-- and Incr is 1
Put_Line ("Using defaults for Increment_By is "
& Integer'Image (C));
A := 10;
B := 3;
C := Increment_By (A, B);
-- ^ Regular parameter passing
Put_Line ("Increment of "
& Integer'Image (A)
& " with "
& Integer'Image (B)
& " is "
& Integer'Image (C));
A := 20;
B := 5;
C := Increment_By (I => A,
Incr => B);
-- ^ Named parameter passing
Put_Line ("Increment of "
& Integer'Image (A)
& " with "
& Integer'Image (B)
& " is "
& Integer'Image (C));
end Show_Increment;
Ada는 매개변수를 전달할 때 기본값이 있든 없든 이름을 붙일 수 있게 해줘요. 규칙이 몇 가지 있어요:
- 위치 매개변수(positional parameter)가 먼저 와야 해요.
- 위치 매개변수는 이름 붙은 매개변수(named parameter) 뒤에 올 수 없어요.
- 관례상, 함수의 해당 매개변수에 기본값이 있다면 호출부에서 매개변수 이름을 붙이는 게 일반적이에요. 다만 코드가 더 명확해진다면 모든 매개변수에 이름을 붙이는 것도 완벽히 받아들여져요.
중첩 서브프로그램 (Nested subprograms)
앞서 잠깐 언급했듯 Ada는 서브프로그램 안에 다른 서브프로그램을 선언할 수 있게 해줘요. 이는 두 가지 이유로 유용해요:
- 프로그램을 더 깔끔하게 구성할 수 있게 해줘요. 서브프로그램이 다른 서브프로그램의 "도우미"로만 필요하다면, 지역화(localization) 원칙에 따라 도우미 서브프로그램은 중첩으로 선언해야 해요.
- 중첩 서브프로그램이 바깥 스코프에 선언된 매개변수와 지역 변수에 접근할 수 있으므로, 통제된 방식으로 상태를 쉽게 공유할 수 있게 해줘요.
앞선 예시의 중복 코드(Put_Line 호출)를 별도 프로시저로 옮길 수 있어요. 중첩된 Display_Result 프로시저가 있는 축약 버전이에요:
with Ada.Text_IO; use Ada.Text_IO;
with Increment_By;
procedure Show_Increment is
A, B, C : Integer;
procedure Display_Result is
begin
Put_Line ("Increment of "
& Integer'Image (A)
& " with "
& Integer'Image (B)
& " is "
& Integer'Image (C));
end Display_Result;
begin
A := 10;
B := 3;
C := Increment_By (A, B);
Display_Result;
A := 20;
B := 5;
C := Increment_By (A, B);
Display_Result;
end Show_Increment;
함수 호출 (Function calls)
Ada 함수 호출의 중요한 특성은 호출 시 반환 값을 무시할 수 없다는 점이에요. 즉 함수 호출은 문장으로 사용될 수 없어요. 함수를 호출하고 그 결과가 필요 없다면, 명시적으로 지역 변수에 저장해야 해요.
function Quadruple (I : Integer)
return Integer;
function Quadruple (I : Integer)
return Integer is
function Double (I : Integer)
return Integer is
begin
return I * 2;
end Double;
Res : Integer;
begin
Double (Double (I));
-- ERROR: cannot use call to function
-- "Double" as a statement
Res := Double (Double (I));
-- OK: return value of Double is
-- assigned to Res
return Res;
end Quadruple;
Double (Double (I)); 같은 문장은 반환 값을 변수에 대입하지 않기 때문에 잘못됐어요. Res := Double (Double (I));로 써서 고칠 수 있어요.
GNAT 툴체인에서: GNAT에서 모든 경고를 활성화하면 미사용 변수가 플래그 되므로 함수의 결과를 무시하기가 더 어려워져요. 예를 들어 다음 코드는 유효하지 않아요:
function Read_Int
(Stream : Network_Stream;
Result : out Integer) return Boolean;
procedure Main is
Stream : Network_Stream := Get_Stream;
My_Int : Integer;
-- Warning: in the line below, B is
-- never read.
B : Boolean := Read_Int (Stream, My_Int);
begin
null;
end Main;
이 경고를 잠재우는 두 가지 해결책이 있어요:
- 변수에
pragma Unreferenced를 사용하거나:
B : Boolean := Read_Int (Stream, My_Int);
pragma Unreferenced (B);
- 변수 이름에 (대소문자 무시)
discard,dummy,ignore,junk,unused중 하나의 문자열을 포함시키는 거예요.
매개변수 모드 (Parameter modes)
지금까지 Ada가 안전 중심의 언어임을 보았어요. 이를 실현하는 방법은 많지만 중요한 두 가지는:
- Ada는 사용자가 프로그램에 기대하는 동작에 대해 가능한 한 많이 지정하게 해서, 불일치가 있으면 컴파일러가 경고하거나 거부할 수 있게 해요.
- Ada는 포인터와 동적 메모리 관리의 일반성·유연성을 달성하기 위한 다양한 기법을 제공하되, 메모리 누수나 댕글링 참조 같은 단점은 없게 해요.
매개변수 모드는 위 두 설계 목표를 달성하는 데 도움을 주는 기능이에요. 서브프로그램 매개변수는 다음 중 하나인 모드로 지정할 수 있어요:
in: 매개변수는 읽기만 가능하고 쓰기는 불가능해요.out: 매개변수에 쓴 다음 읽을 수 있어요.in out: 매개변수를 읽고 쓸 수 있어요.
매개변수의 기본 모드는 in이에요. 지금까지의 대부분 예시가 in 매개변수를 사용했어요.
역사적으로: 함수와 프로시저는 원래 철학이 더 달랐어요. Ada 2012 이전에는 함수가 in 매개변수만 받을 수 있었어요.
in 매개변수 (In parameters)
첫 번째 모드는 지금까지 암묵적으로 사용해 온 것이에요. 이 모드로 전달된 매개변수는 수정될 수 없어서, 다음 프로그램은 오류를 일으켜요:
procedure Swap (A, B : Integer) is
Tmp : Integer;
begin
Tmp := A;
-- Error: assignment to "in" mode
-- parameter not allowed
A := B;
-- Error: assignment to "in" mode
-- parameter not allowed
B := Tmp;
end Swap;
in이 기본 모드라는 사실은 매우 중요해요. 수정이 허용되는 모드를 명시적으로 지정하지 않는 한 매개변수가 수정되지 않는다는 뜻이기 때문이에요.
in out 매개변수 (In out parameters)
위 코드를 고치려면 in out 매개변수를 사용할 수 있어요.
with Ada.Text_IO; use Ada.Text_IO;
procedure In_Out_Params is
procedure Swap (A, B : in out Integer) is
Tmp : Integer;
begin
Tmp := A;
A := B;
B := Tmp;
end Swap;
A : Integer := 12;
B : Integer := 44;
begin
Swap (A, B);
-- Prints 44
Put_Line (Integer'Image (A));
end In_Out_Params;
in out 매개변수는 매개변수로 전달된 객체에 읽기·쓰기 접근을 허용해요. 그래서 위 예시에서 Swap 호출 후 A가 수정되는 걸 볼 수 있어요.
주의: in out 매개변수는 C++의 참조나 Java의 참조 전달 방식의 일반 매개변수처럼 보이지만, Ada 언어 표준은 특정 범주의 타입(나중에 설명)을 제외하고 in out 매개변수에 "참조 전달"을 강제하지 않아요. 일반적으로 모드를 값 전달 vs 참조 전달 의미론보다 더 높은 수준으로 생각하는 게 좋아요. 컴파일러 입장에서 in 매개변수로 전달된 배열은 참조로 전달될 수 있는데(매개변수가 대입 불가능하므로 사용자에게는 아무런 변화가 없어요), 이산형 타입의 매개변수는 모드와 무관하게 항상 복사로 전달돼요(대부분 아키텍처에서 더 효율적이기 때문이에요).
out 매개변수 (Out parameters)
out 모드는 서브프로그램이 호출 시점에 초기화되지 않을 수 있는 매개변수에 써야 할 때 적용돼요. out 매개변수의 값을 읽는 것은 허용되지만, 서브프로그램이 매개변수에 값을 할당한 후에만 해야 해요. out 매개변수는 함수의 반환 값처럼 동작해요. 서브프로그램이 반환될 때 실제 매개변수(변수)는 반환 시점의 out 매개변수 값을 갖게 되요.
Ada에는 튜플 구조가 없고 서브프로그램에서 여러 값을 반환할 수 없어요(완전한 레코드 타입을 선언하지 않는 한). 그래서 서브프로그램에서 여러 값을 반환하는 한 가지 방법은 out 매개변수를 쓰는 거예요.
예를 들어 네트워크에서 정수를 읽는 프로시저는 다음 선언 중 하나를 가질 수 있어요:
procedure Read_Int
(Stream : Network_Stream;
Success : out Boolean;
Result : out Integer);
function Read_Int
(Stream : Network_Stream;
Result : out Integer) return Boolean;
out 변수를 쓰기 전에 읽는 것은 이상적으로 오류를 일으켜야 하지만, 그것을 규칙으로 강제하면 비효율적인 런타임 검사나 복잡한 컴파일 타임 규칙이 필요해져요. 그래서 사용자 관점에서 out 매개변수는 서브프로그램이 호출될 때 초기화되지 않은 변수처럼 동작해요.
GNAT 툴체인에서: GNAT는 out 매개변수 잘못 쓰기의 간단한 경우를 감지해요. 예를 들어 다음 프로그램에는 컴파일러가 경고를 내요:
procedure Outp is
procedure Foo (A : out Integer) is
B : Integer := A;
-- ^ Warning on reference
-- to uninitialized A
begin
A := B;
end Foo;
begin
null;
end Outp;
서브프로그램의 전방 선언 (Forward declaration)
앞서 보았듯 서브프로그램은 완전히 정의되지 않고 선언될 수 있어요. 이는 일반적으로 가능하며, 아래 예시처럼 서브프로그램들이 상호 재귀적이어야 할 때 유용해요:
procedure Mutually_Recursive_Subprograms is
procedure Compute_A (V : Natural);
-- Forward declaration of Compute_A
procedure Compute_B (V : Natural) is
begin
if V > 5 then
Compute_A (V - 1);
-- Call to Compute_A
end if;
end Compute_B;
procedure Compute_A (V : Natural) is
begin
if V > 2 then
Compute_B (V - 1);
-- Call to Compute_B
end if;
end Compute_A;
begin
Compute_A (15);
end Mutually_Recursive_Subprograms;
이름 변경 (Renaming)
renames 키워드를 사용해 서브프로그램에 새 이름을 선언함으로써 이름을 바꿀 수 있어요:
procedure New_Proc renames Original_Proc;
이것은 예를 들어 시스템에서 변경할 수 없는 외부 소스의 코드를 사용할 때 애플리케이션의 가독성을 높이는 데 유용해요. 예시를 볼게요:
procedure A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed
(A_Message : String);
with Ada.Text_IO; use Ada.Text_IO;
procedure A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed
(A_Message : String) is
begin
Put_Line (A_Message);
end A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
위 프로시저 이름의 표현이 의미하듯 우리는 그 이름을 바꿀 수 없어요. 하지만 테스트 애플리케이션에서 Show 같은 이름으로 변경하고 이 짧은 이름을 사용할 수는 있어요. 원래 서브프로그램의 모든 매개변수도 선언해야 하며, 선언에서 매개변수 이름도 바꿀 수 있어요. 예를 들어:
with A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
procedure Show_Renaming is
procedure Show (S : String) renames
A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
begin
Show ("Hello World!");
end Show_Renaming;
Show 프로시저를 선언한 후에도 원래 이름(A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed)이 여전히 보인다는 점을 주목하세요.
표준 라이브러리의 서브프로그램도 이름을 바꿀 수 있어요. 예를 들어 Integer'Image를 Img로 바꿀 수 있어요:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Image_Renaming is
function Img (I : Integer) return String
renames Integer'Image;
begin
Put_Line (Img (2));
Put_Line (Img (3));
end Show_Image_Renaming;
이름 변경은 원래 선언에 없던 기본 표현식도 도입할 수 있게 해줘요. 예를 들어 Show 프로시저 String 매개변수의 기본값으로 "Hello World!"를 지정할 수 있어요:
with A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
procedure Show_Renaming_Defaults is
procedure Show (S : String := "Hello World!")
renames
A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
begin
Show;
end Show_Renaming_Defaults;