서브프로그램

서브프로그램 (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'ImageImg로 바꿀 수 있어요:

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;

더 알아보기 (Learn more)