[!attention] 이 장의 목표
- 프로시저 작성 — 함수와 서브루틴을 정의하고 호출한다.
- 프로시저 위치 구분 — 내부·외부·모듈 프로시저의 차이를 안다.
- 반환값 다루기 — result로 함수의 반환값을 명시한다.
- 재귀함수 익히기 — recursive 함수로 계승(factorial) 문제를 푼다.
지금까지 배운모든 코드는 하나의 program 으로 작성하였다. 프로그램이 짧을 때는 상관없지만, 같은 계산을 여러 번 반복 때나, 하나의 식을 여러 입력값에 대해 평가해야 할 때는 문제가 복잡해 진다. 이 경우, 계산식을 별도의 코드에 정의해 두고 주 프로그램(host)에서 불러내어(call) 사용할 수 있다면, 코드 길이를 줄여 가독성 높게 만들 수 있다.
프로시저(procedure) 가 이 문제를 해결할 수 있다. 프로시저는 이름이 붙은 코드 묶음으로, 함수(function) 와 서브루틴(subroutine) 두 종류가 있다. 한 번만 프로시저로 정의해 두면, 해당 프로시저의 이름만 호출하여 같은 논리 구조를 몇 번이고 반복 사용할 수 있다. 프로시저는 되부름(recursion) 함수를 사용하여 자기 자신을 호출할 수도 있다.
[!important] 10.1 함수와 서브루틴의 정의·호출
프로시저는 "입력을 받아 일을 하고 결과를 돌려주는 부품으로 정의할 수 있다. 함수와 서브루틴은 프로그램의 기능을 독립적인 단위로 분할하고 재사용성을 높이는 코드 모듈화의 핵심 도구이다.
함수와 서브루틴은 결과를 돌려주는 방식에서 다음 명확한 차이가 있다.
반환값의 개수: 함수는 정확히 1개의 값만을 반환하지만, 서브루틴은 인수를 통하여 0개 또는 여러 개의 값을 돌려준다.
호출 방법: 함수는 $y = f(x)$와 같이 식 안에서 직접 사용되는 반면, 서브루틴은 문장 형태로 call s(...)와 같이 호출된다.
주 용도: 함수는 특정 값을 계산하여 돌려주는 데 집중되어 있으며, 서브루틴은 여러 결과를 동시에 산출하거나 다양한 부수 작업을 처리하는 데 활용된다.
프로시저의 인수 전달 메커니즘을 이해하기 위해 아래 주요 용어들을 익혀두자.
가인수 (dummy argument): 프로시저를 정의할 때 사용하는 변수이다. 프로시저 내부에서 실제 데이터인 실인수를 대신하여 메모리 공간(stack)을 차지하는 역할을 한다.
실인수 (actual argument): 프로시저를 호출할 때 실제로 전달하는 구체적인 값이나 변수이다.
매핑 원리 (mapping principle): 프로시저가 호출될 때, 실인수는 정의된 가인수와 위치 순서에 따라 일대일로 매칭되어 전달된다.
함수는 입력값을 받아 계산한 후, 단 하나의 결과값을 반환할 때 사용한다. 아래와 같이 변수 = 함수_이름([실인수_목록])과 같이 식의 일부나 대입문 우변에서 호출한다.
[형] function 이름 ( [가인수목록] ) [result(결과변수)]
implicit none
선언부
실행부 — 결과변수(또는 이름)에 값을 대입
end function [이름]
1장에서 구현했던 온도 변화 예제를 함수를 써서 구현해 보자. 함수는 주 프로그램 하단에 contains 키워드를 쓰고 그 아래에 정의한다.
%%writefile temperature.f90
program temperature
implicit none
integer :: i
real :: celsius, fahr
print '(a)', " celsius fahrenheit"
do i = 0, 100, 20
celsius = real(i)
fahr = to_fahrenheit(celsius)
print '(f9.1, f12.1)', celsius, fahr
end do
contains
function to_fahrenheit(c) result(f)
implicit none
real :: c
real :: f
f = c * 9.0 / 5.0 + 32.0
end function to_fahrenheit
end program temperature
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall temperature.f90 -o temperature
!./temperature
실행 결과:
celsius fahrenheit
0.0 32.0
20.0 68.0
40.0 104.0
60.0 140.0
80.0 176.0
100.0 212.0
Fortran에서 contains 아래에 정의된 내부 함수는 바깥쪽 주 프로그램의 자원을 공유할 수 있는 권한을 가진다. 이를 호스트 연관(host association) 이라고 한다.
함수 안의 변수 (c, f): 이 변수들은 함수 내부에서 선언되었기 때문에 함수 바깥(주 프로그램)에서는 인식되지 않는 완전히 독립된 변수이다.
주 프로그램의 변수 (i, celsius, fahr): 반대로 주 프로그램에 선언된 변수들은 함수 안에서 그대로 가져다 쓸 수 있다.
위 예제에서는 함수가 주 프로그램의 변수를 사용하지 않고 함수 내에서 독립적으로 c와 f를 선언해서 사용했다. 이는 예기치 않은 부작용을 막아주는 매우 안전하고 권장되는 코딩 방식이다.
[!warning] [흔한 실수] 만약 주 프로그램 내 변수명과 함수 내 새로 선언한 변수명 같다면?
주 프로그램과 함수 내부에서 같은 이름의 변수를 선언하면, 함수 내부에서 선언된 변수가 지역 변수로서 우선 적용되어 바깥의 변수를 가려버린다(information hiding). 따라서 함수 안에서 그 이름을 사용하거나 값을 변경하더라도, 이는 함수 내부의 지역 변수에만 영향을 미치며 주 프로그램의 변수에는 아무런 변화가 없다.
예를 들어, 주 프로그램에
real :: c가 있고, 내부 함수 안에도 똑같이real :: c를 선언한다면, 함수 안에서 값을 바꾸면 함수 내부의 변수만 바뀔 뿐, 주 프로그램의 변수에는 아무런 영향을 주지 않는다.
서브루틴은 값을 반환하지 않거나, 여러 개의 인수를 통해 다수의 결과값을 변경 하거나 전달할 때 사용한다. 아래와 같이 독립된 문장으로 call 서브루틴_이름([실인수_목록]) 형식을 사용하여 호출한다.
subroutine 이름 ( [가인수목록] )
implicit none
선언부
실행부
end subroutine [이름]
원의 넓이와 둘레를 한 번에 구하고 싶다고 하자. 함수는 단 하나의 값만 반환할 수 있으므로 원의 넓이와 둘레를 각각 구하려면 두 번의 함수 호출이 필요하다. 반면 서브루틴은 여러 개의 인수를 활용하여 두 가지 이상의 계산 결과를 한 번에 돌려줄 수 있어 효율적이다.
%%writefile circle_demo.f90
program circle_demo
implicit none
real :: r, area, circumference
r = 2.5
call circle(r, area, circumference)
print '(a, f0.4)', "radius = ", r
print '(a, f0.4)', "area = ", area
print '(a, f0.4)', "circumference = ", circumference
contains
subroutine circle(radius, a, c)
implicit none
real :: radius, a, c
real, parameter :: pi = 3.14159265
a = pi * radius**2
c = 2.0 * pi * radius
end subroutine circle
end program circle_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall circle_demo.f90 -o circle_demo
!./circle_demo
실행 결과:
radius = 2.5000
area = 19.6350
circumference = 15.7080
서브루틴 circle은 하나의 입력값(radius)을 받아 연산을 수행한 후, 그 결과를 a와 c에 각각 저장한다. 이때, 호출하는 주 프로그램의 실인수인 area와 circumference가 서브루틴 내부의 가인수인 a, c와 순서대로 일대일 매핑된다. 따라서, 서브루틴 내부에서 a와 c에 대입한 연산 결과가 호출한 바깥쪽 프로그램의 변수들에 그대로 반영된다.
반지름의 크기가 2.5000으로 지정되었을 때, 원의 넓이는 원주율과 반지름의 제곱을 곱하여 19.6350으로 계산된다.
동일한 반지름인 2.5000을 기준으로 원의 둘레를 계산하면 지름에 원주율을 곱한 값인 15.7080이 산출된다.
이 예제는 연산의 오버헤드를 줄이고 관련성 높은 데이터를 한 번에 처리하는 서브루틴의 다중 결과 반환 메커니즘을 잘 보여준다.
[!warning] [흔한 실수] 함수와 서브루틴을 혼동 함수는 식 안에서
y = f(x)로 쓰고, 서브루틴은call s(...)로 부른다. 함수를call로 부르거나, 서브루틴을 식 한가운데(y = s(x) + 1)에서 쓰면 컴파일 오류가 난다. "값을 돌려받아 식에 쓰는가(함수)", "호출해서 일을 시키는가(서브루틴)"로 구분하자.
[!important] 10.2 내부·외부·모듈 프로시저
프로시저에는 다음과 같이 세 가지 종류가 있다.
contains 문 아래 정의된다.contains 문 아래 정의된다.이 프로시저들은 주 프로그램 등에서 호출하여 사용하는데, 호출에 필요한 형식적 규약이나 구조(인수 개수, 타입, 반환값 등)를 인터페이스(interface)라고 한다. 즉, 프로시저는 실제 동작을 수행하는 코드 단위(함수, 서브루틴) 실체이고 인터페이스는 그 실체를 어떻게 불러 쓰는지에 대한 약속이다.
이 절에서는 내부와 외부 프로시저를 설명하고, 모듈 프로시저는 12장에서 다룬다.
내부 프로시저는 프로그램 본체 뒤에 contains를 두고 그 아래에 정의한다. 내부 프로시저의 장점은 바깥 프로그램(호스트)에 선언된 변수를 인수로 넘기지 않아도 그대로 읽고 쓸 수 있다는 호스트 결합(host association) 이다.
%%writefile host_assoc.f90
program host_assoc
implicit none
integer :: counter
counter = 0
call bump()
call bump()
call bump()
print '(a, i0)', "counter = ", counter
contains
subroutine bump()
implicit none
counter = counter + 1 ! counter comes from the host
end subroutine bump
end program host_assoc
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall host_assoc.f90 -o host_assoc
!./host_assoc
실행 결과:
counter = 3
주 프로그램이 시작되면 정수형 변수 counter의 0으로 초기화된다.
인수가 없는 서브루틴인 bump()를 첫 번째로 호출하면, 호스트 결합을 통해 주 프로그램의 변수 counter를 직접 인지하여 그 수치를 1로 증가시킨다.
이어서 bump()를 두 번째와 세 번째로 연달아 호출하면서, counter의 수치는 각각 2와 3으로 1씩 누적되어 증가한다.
모든 호출이 완료된 후, 최종적으로 counter의 결과는 정확히 3이 된다.
외부 프로시저는 주 프로그램이나 모듈 등 그 어떤 구조적 단위에도 포함되지 않고 물리적으로 완전히 독립되어 존재하는 프로시저이다. 구조 자체는 직관적이고 단순하지만, 외부 함수를 호출하여 사용할 때는 호출하는 주 프로그램 등에서 해당 함수가 반환하는 데이터 타입(형)을 반드시 명시적으로 선언해 주어야 한다.
%%writefile external_demo.f90
program external_demo
implicit none
real :: triple ! must declare the external function's type
print '(f0.2)', triple(4.0)
end program external_demo
function triple(x)
implicit none
real :: x
real :: triple
triple = 3.0 * x
end function triple
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall external_demo.f90 -o external_demo
!./external_demo
실행 결과:
12.00
주 프로그램 내부에서 외부 함수인 triple이 실수형(real) 값을 반환한다는 사실을 real :: triple 문장을 통해 컴파일러에게 미리 알려준다.
주 프로그램 실행 중 실인수 4.0이라는 실수 값을 전달하며 triple(4.0)을 호출한다.
외부 함수 triple은 전달받은 인수 $4.0$에 3.0을 곱하는 연산을 수행하여 12.0이라는 최종 값을 산출한다.
이후, 주 프로그램은 $12.0$ 이라는 반환된 결과를 받아 소수점 이하 둘째 자리까지 지정된 형식에 맞춰 $12.00$ 이라는 수치를 정확하게 출력한다.
만약 주 프로그램 내부에서 real :: triple이라는 형식 선언을 누락하면 컴파일러는 외부 함수가 어떤 형태의 데이터를 돌려주는지 전혀 알 수 없다. 이 경우 컴파일러는 반환 타입이 일치하지 않는다는 의미를 담은 다음 오류 메시지를 발생시키며 컴파일을 중단한다.
Error: Return type mismatch of function 'triple' ... (UNKNOWN/REAL(4))
이런 현상이 발생하는 이유는 외부 프로시저가 호출하는 주 프로그램과 분리되어 있어 컴파일러가 그 내부 구조를 자동으로 인지하지 못하는 묵시적 인터페이스(implicit interface) 상태이기 때문이다. 이와 같은 선언 누락의 위험성을 방지하고 프로시저를 안전하게 관리하기 위해서는 명시적 인터페이스(explicit interface)가 확보되어야 한다.
따라서 특별한 이유가 없다면 컴파일러가 함수의 구조와 타입을 자동으로 검증할 수 있는 내부 프로시저나 모듈 프로시저를 기본적으로 사용하는 것이 안전하다.
[!warning] [흔한 실수] 프로시저에도
implicit none을 빠뜨리지 말 것.implicit none는 프로시저마다 각각 따로 써야 한다. 아래 함수는a에 넣으려던 값을 오타ar에 넣었는데,implicit none이 없어ar이 새 변수로 묵인된다. 결과a는 값이 정해지지 않은 채 쓰레기값(garbage value)을 돌려준다.
fortran function area(r) result(a) real :: r, a ar = 3.14159 * r * r ! typo: ar instead of a a = a ! a is never assigned end function area
implicit none을 넣으면 컴파일러가 오류를 찾아내고, 아래와 같은 메세지를 출력한다.Error: Symbol 'ar' ... has no IMPLICIT type; did you mean 'a'?
[!important] 10.3 반환값과 result
Fortran에서 함수가 연산 결과를 호출하는 프로그램(예: 주 프로그램)으로 돌려주는 방법에는 두 가지 표기 방식이 있다.
방식 A. 함수 이름에 대입 (전통적 방식) 함수명 자체를 내부 변수처럼 취급하여, 주 프로그램 본문 안에서 함수명에 직접 최종 계산값을 대입하는 방식이다. 별도의 결과 변수를 지정하지 않는 간결한 구조일 때 주로 사용한다.
방식 B. result 절 사용 (현대적 방식)
함수 선언부 뒤에 result(결과_변수) 구문을 추가하여, 반환값을 저장할 독립된 변수를 따로 정의하는 방식이다. 함수 이름과 반환 변수를 명확히 분리하므로 코드의 가독성이 높아지며, 특히 함수가 자기 자신을 다시 호출하는 재귀 함수(Recursive Function)를 구현할 때는 이 방식을 사용한다.
%%writefile two_styles.f90
program two_styles
implicit none
real :: x
x = 3.0
print '(a, f6.2)', "square(x) = ", square(x)
print '(a, f6.2)', "cube(x) = ", cube(x)
contains
! style A: return value through the function name
real function square(a)
implicit none
real :: a
square = a * a
end function square
! style B: return value through a result variable
function cube(a) result(y)
implicit none
real :: a
real :: y
y = a * a * a
end function cube
end program two_styles
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall two_styles.f90 -o two_styles
!./two_styles
실행 결과:
square(x) = 9.00
cube(x) = 27.00
주 프로그램에서 입력값 변수 x의 초기 수치를 3.0으로 설정하고 연산을 시작한다.
전통적인 방식 A를 적용한 square(x) 함수를 호출하면, 수치 3.0의 제곱을 계산하여 9.00이라는 결과를 출력한다.
현대적인 방식 B를 적용한 cube(x) 함수를 호출하면, 수치 3.0의 세제곱을 계산하여 27.00이라는 결과를 정확하게 출력한다.
방식 A는 real function square(a)와 같이 함수 헤더에 반환할 데이터 타입을 직접 명시한다. 반면 방식 B는 함수의 헤더에 타입을 적지 않고, result(y) 구문으로 지정한 결과 변수 y의 타입을 함수 내부의 선언부(real :: y)에서 정의한다.
방식 B를 사용하여 결과 변수에 함수 이름과 전혀 다른 직관적인 이름을 부여하면, 복잡한 함수 본문의 코드를 훨씬 읽기 쉽도록 작성할 수 있다.
[!warning] [흔한 실수] 타입 중복 선언 금지 함수 이름과
result로 지정된 결과 변수는 컴퓨터 내부에서 완전히 동일한 데이터 타입과 종류 속성을 공유한다. 따라서 두 곳 중 단 한 곳에만 데이터 타입을 선언해야 하며, 만약 함수 헤더와 내부 선언부 양쪽에 모두 타입을 선언하면 컴파일러는 중복 선언 오류를 낸다.
[!important] 10.4 재귀(recursive) 함수와 result
재귀(recursion, 되부름)은 프로시저가 직접 또는 간접적으로 자기 자신을 호출하는 것이다. 이는 큰 문제를 동일한 형태의 더 작은 문제로 쪼개어 해결하는 방식으로, 코드가 간결하고 논리 구조가 깔끔해 진다. 다만 호출 횟수가 많아지면 메모리 사용과 실행 시간이 추가로 소모될 수도 있다.
재귀 함수를 구현할 때는 다음 문법적 규칙을 철저히 따라야 한다.
키워드 명시: 해당 함수가 재귀적으로 작동한다는 사실을 컴파일러에게 알리기 위해 반드시 recursive 키워드를 명시해야 한다.
result 절의 필수화: 되부름 함수는 연산 결과를 함수 이름 자체로 반환할 수 없다. 함수 이름은 자기 자신을 호출하는 식의 일부로 사용되어야 하므로, 반환값을 담을 별도의 결과 변수를 result 절을 통해 반드시 지정해야 한다. 이때 함수 이름과 결과 변수는 컴퓨터 내부에서 동일한 속성을 공유한다.
재귀 함수 선언에서, 다음 세 가지 표기 방식은 모두 문법적으로 동일하다.
| 표기 방식 | 문법 예시 |
|---|---|
| 방식 1 | integer recursive function 이름 (가인수) result(결과변수) |
| 방식 2 | recursive integer function 이름 (가인수) result(결과변수) |
| 방식 3 | recursive function 이름 (가인수) result(결과변수) |
이 책에서는 구조적 명료성을 위해 아래와 같이 방식 3을 사용한다.
recursive function factorial(n) result(res)
integer, intent(in) :: n
integer :: res ! 결과 변수의 형을 선언부에 명시
if (n <= 1) then
res = 1
else
res = n * factorial(n - 1)
end if
end function factorial
재귀 함수의 특징과 사용 방법은 다음과 같다.
유연한 선언: recursive 키워드는 함수의 자료형 앞이나 뒤 등 선언부의 여러 위치에 자유롭게 배치될 수 있다. 그러나 가독성과 유지보수성을 높이기 위해서 개발자 구성원들이 합의한 일관된 표기 방식을 선택하는 것이 좋다.
결과 변수의 필수성: 재귀 함수는 단계별 연산 과정에서 자기 자신을 지속적으로 다시 호출하며 값을 누적하거나 최종 값을 반환한다. 함수 이름 자체가 호출에 사용되므로, result 를 사용하여 독립된 결과 변수를 명시적으로 지정해야 한다.
종료 조건: 재귀 함수를 구현할 때는 더 이상 자신을 호출하지 않고 제어를 마치는 종료 조건( 기저 사례, base case)이 반드시 포함되어야 한다. 만약 이 종료 조건이 누락되면 프로그램은 무한히 함수를 호출하게 되며, 종국에는 메모리 허용 범위를 초과하여 시스템이 강제로 중단되는 스택 오버플로우(stack overflow) 오류를 발생시킨다.
재귀함수의 가장 전형적인 예제인 계승을 계산하는 예제 코드를 구현해 보자.
정수 n의 계승 n! = n × (n-1) × … × 1을 되부름으로 계산한다. n이 1 이하이면 결과는 1이고(종료 조건), 그렇지 않으면 n × (n-1)!로 더 작은 문제에 맡긴다.
%%writefile factorial_demo.f90
program factorial_demo
implicit none
integer :: n
print '(a, i0)', "fact(5) = ", fact(5)
print '(a)', "----------------"
do n = 0, 8
print '(i2, a, i7)', n, "! = ", fact(n)
end do
contains
recursive function fact(n) result(n_fact)
implicit none
integer :: n
integer :: n_fact
if (n <= 1) then
n_fact = 1
else
n_fact = n * fact(n - 1)
end if
end function fact
end program factorial_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall factorial_demo.f90 -o factorial_demo
!./factorial_demo
실행 결과:
fact(5) = 120
----------------
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
프로그램 내부에서 fact(5)가 호출되면 컴퓨터는 즉시 결과를 내지 못하고 $5 \times \text{fact}(4)$라는 연산식을 메모리에 쌓아둔다. 이어서 fact(4)는 $4 \times \text{fact}(3)$으로, fact(3)은 $3 \times \text{fact}(2)$로, fact(2)는 $2 \times \text{fact}(1)$로 문제를 점진적으로 축소하며 자기 자신을 연속적으로 호출한다.
최종적으로 인수가 1에 도달하는 순간, if (n <= 1)이라는 종료 조건이 참이 되면서 fact(1)은 조건 없이 1을 반환한다. 이 시점부터 메모리에 역순으로 쌓여 있던 연산들이 $2 \times 1 = 2$, $3 \times 2 = 6$, $4 \times 6 = 24$, $5 \times 24 = 120$의 역순으로 풀려나가며 최종 결과인 $120$ 을 완성한다.
이후, 반복문을 통해 0부터 8까지의 계승을 순차적으로 계산한 결과가 다음으로 나타난다.
수학적 정의에 따라 0!과 1!의 결과값은 모두 종료 조건(base case)에 해당하여 1이 된다.
이어서 2!은 2, 3!은 6, 4!은 24로 증가하며, 5!은 120, 6!은 720, 7!은 5040을 거쳐 최종 8!의 결과값은 $40320$ 으로 산출된다.
[!warning] [흔한 실수] 되부름 함수·서브루틴에는
recursive를 붙일 것. 자기 자신을 호출하는 프로시저는 함수 머리에recursive를 붙여야 한다.recursive키워드를 누락하거나 반환을 위한result을 정상적으로 선언하지 않으면,cannot be called recursively, as it is not RECURSIVE오류가 난다. 참고로, Fortran 2018부터는 프로시저가 기본적으로 되부름 가능해져recursive키워드가 필수는 아니다.
사용자가 정의한 함수를 활용하여 물리적인 감쇠 진동 곡선의 데이터를 생성하고, 이를 파일로 저장하여 시각화하는 전체적인 과정을 보여주는 예제이다.
%%writefile plot_function.f90
program plot_function
implicit none
call sample_to_csv(400, "curve.csv")
print '(a)', "curve.csv written"
contains
function damped(x) result(y)
implicit none
real :: x
real :: y
y = exp(-0.1 * x) * sin(x)
end function damped
subroutine sample_to_csv(npoints, filename)
implicit none
integer :: npoints
character(len=*) :: filename
integer :: i, u
real :: x
real, parameter :: xmax = 20.0
open(newunit=u, file=filename, status="replace", action="write")
do i = 0, npoints
x = real(i) / real(npoints) * xmax
write(u, '(f10.5, ",", f12.6)') x, damped(x)
end do
close(u)
end subroutine sample_to_csv
end program plot_function
감쇠 진동 f(x) = e^(-0.1x)·sin(x)를 함수로 정의하고, 서브루틴이 그 함수를 여러 지점에서 평가해 curve.csv에 저장한다. 내부 프로시저끼리 호출하는 방법에 주목하자. 서브루틴 sample_to_csv가 같은 호스트의 함수 damped를 호출한다.
contains 문 아래에 함수 damped와 서브루틴 sample_to_csv라는 두 개의 내부 프로시저를 동시에 정의하고 있다. 주목할 점은, 서브루틴 sample_to_csv 내부의 반복문 안에서 damped(x) 함수를 직접 호출하여 사용한다는 것이다. 동일한 주 프로그램 공간에 속한 내부 프로시저들은 호스트를 공유하므로, 별도의 복잡한 연결 과정 없이도 이처럼 서로를 자유롭게 호출하여 협력 연산을 수행할 수 있다.
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall plot_function.f90 -o plot_function
!./plot_function
실행 결과 :
curve.csv written
!head curve.csv 명령으로 curve.csv의 앞부분 출력:
0.00000, 0.000000
0.05000, 0.049730
0.10000, 0.098840
0.15000, 0.147213
0.20000, 0.194735
0.25000, 0.241296
0.30000, 0.286786
0.35000, 0.331104
0.40000, 0.374149
0.45000, 0.415826
독립변수 $x$의 수치가 $0.00000$ 인 시작 지점에서, 감쇠 진동 함수의 계산 결과인 $y$값은 $0.000000$ 으로 산출된다.
$x$의 수치가 $0.05000$ 으로 미세하게 증가함에 따라, 함수의 결과값은 0.049730으로 상승한다.
이어서 $x$의 수치가 $0.10000$ 에 도달했을 때, 대응하는 함수의 결과값은 $0.098840$ 으로 연속적으로 증가하는 흐름을 보여준다.
이와 같은 방식으로 총 400개의 지점에서 $x$값을 0부터 20.0까지 일정하게 변화시키며 계산된 수치 쌍이 데이터 파일curve.csv에 순차적으로 기록된다.
이제 파이썬 코드로 시각화해 보자.
import csv
import matplotlib.pyplot as plt
xs, ys = [], []
with open("curve.csv") as fp:
for row in csv.reader(fp):
xs.append(float(row[0]))
ys.append(float(row[1]))
plt.figure(figsize=(6, 4.5))
plt.plot(xs, ys)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("Fortran function, Python plot")
plt.grid(True)
plt.savefig("curve.png", dpi=120)
plt.show()
수학적으로 $f(x) = e^{-0.1x} \sin(x)$ 식은 시간이 흐름에 따라 진폭이 점차 줄어드는 감쇠 진동(damped oscillation)을 나타낸다.
사전에 정의된 damped 함수 내부의 계산 식 한 줄만 원하는 수식으로 변경하면, 데이터 생성 서브루틴이나 시각화 코드를 전혀 수정하지 않고도 다른 함수의 그래프를 즉시 얻을 수 있다. 이것이 바로 계산 프로그램을 프로시저로 분리하여 모듈화했을 때 얻을 수 있는 가장 강력한 구조적 이점이다.
![[Pasted image 20260712153058.png]]
[!important] 오류 학습
프로시저를 처음 배울 때 가장 자주 겪는 실수는 인수를 잘못 넘기는 것이다. 인수의 개수를 빠뜨리거나, 데이터 타입이 다른 값을 넘기는 경우가 대표적이다. 다행히 내부 프로시저는 컴파일러가 그 구조(인터페이스)를 미리 알고 있으므로, 이런 실수를 컴파일 단계에서 정확히 잡아낸다. 두 가지 전형적인 오류 사례를 직접 확인해 보자.
서브루틴 circle은 세 개의 인수(radius, a, c)를 필요로 한다. 만약 둘레 계산 결과가 필요 없다는 이유로 인수를 두 개만 넘겨 호출하면 오류가 발생한다.
%%writefile eld_arg_count.f90
program edl_arg_count
implicit none
real :: r, area, circ
r = 2.0
call circle(r, area) ! circle needs three arguments
print *, area
contains
subroutine circle(radius, a, c)
implicit none
real :: radius, a, c
real, parameter :: pi = 3.14159265
a = pi * radius**2
c = 2.0 * pi * radius
end subroutine circle
end program edl_arg_count
위 코드를 아래와 같이 컴파일하면 에러 메시지가 출력된다.
!gfortran -O2 -std=f2018 -Wall edl_arg_count.f90 -o edl_arg_count
!./edl_arg_count
eld_arg_count.f90:5:61:
5 | call circle(r, area) ! circle needs three arguments
| 1
Error: Missing actual argument for argument ‘c’ at (1)
/bin/bash: line 1: ./eld_arg_count: No such file or directory
이는 세 번째 가인수인 c에 대응하는 실인수가 전달되지 않았다는 의미이다. 설령 둘레 값이 필요하지 않더라도, 결과 값을 받아줄 변수를 반드시 선언하여 세 개의 인수를 모두 넘겨야 한다. 호출부를 call circle(r, area, circ)로 수정하면 정상적으로 컴파일된다.
함수 to_fahrenheit는 실수형 인수를 받도록 정의되어 있다. 만약 섭씨 100도를 계산하기 위해 정수형 상수인 100을 인수로 넘기면 오류가 발생한다.
%%writefile edl_arg_type.f90
program edl_arg_type
implicit none
print '(f0.2)', to_fahrenheit(100) ! passing integer, dummy is real
contains
function to_fahrenheit(c) result(f)
implicit none
real :: c
real :: f
f = c * 9.0 / 5.0 + 32.0
end function to_fahrenheit
end program edl_arg_type
위 코드를 아래와 같이 컴파일하면 에러 메시지가 출력된다.
!gfortran -O2 -std=f2018 -Wall edl_arg_type.f90 -o edl_arg_type
!./edl_arg_type
edl_arg_type.f90:3:18:
3 | print '(f0.2)', to_fahrenheit(100) ! passing integer, dummy is real
| 1
Error: Type mismatch in argument ‘c’ at (1); passed INTEGER(4) to REAL(4) /bin/bash: line 1: ./edl_arg_type: No such file or directory
이는 가인수 c는 실수형 인터페이스를 가지는데, 실인수로 정수형 변수가 전달되었다는 의미이다. 따라서 이 문제를 해결하려면 실인수를 100.0으로 명시하거나, real(100)과 같이 실수형으로 명시적 변환을 거쳐 전달해야 한다.
위 두 예제에서, 인수의 개수를 빠뜨리거나, 데이터 타입이 다른 값을 넘기는 두 가지 오류 사례 모두 실행 단계가 아닌 컴파일 단계에서 확실하게 걸러졌다는 점이 중요하다. circle과 to_fahrenheit가 모두 주 프로그램 내부에 포함된 내부 프로시저이기 때문에 컴파일러가 인터페이스의 구조를 정확히 파악할 수 있었다.
만약 동일한 프로시저를 명시적 인터페이스 없이 외부 프로시저로 작성했다면, 데이터 타입이 어긋난 호출 문장이 오류 없이 컴파일을 통과하게 된다. 그 결과 프로그램 실행 중에 메모리 해석 오류로 인해 전혀 엉뚱한 값을 출력하는 심각한 논리적 오류를 유발한다. 내부 프로시저나 모듈 프로시저를 기본적으로 사용하도록 권장하는 이유가 바로 여기에 있다. 외부 프로시저를 안전하게 사용하도록 지원하는 명시적 인터페이스의 개념과 구현 방법은 11장에서 다룬다.
[!important] 요약
y = f(x)), 서브루틴은 call로 부르며 인수를 통해 결과를 0개~여러 개 돌려준다.contains 아래에 두면 내부 프로시저가 되고, 호스트의 변수를 그대로 볼 수 있다(호스트 결합).result 절로 돌려준다.recursive를 명시하고, 결과는 result 변수로 돌려준다.implicit none을 따로 써야 하며, 모든 실행 경로에서 결과 변수에 값을 대입해야 한다.| 항목 | 표기 |
|---|---|
| 함수 정의 | function name(args) result(r) … end function |
| 함수 정의(이름 반환) | real function name(args) … end function |
| 서브루틴 정의 | subroutine name(args) … end subroutine |
| 함수 호출 | y = name(args) |
| 서브루틴 호출 | call name(args) |
| 되부름 함수 | recursive function name(args) result(r) |
| 내부 프로시저 | 호스트의 contains 아래에 정의 |
[!important] 연습 문제
to_celsius(f)를 작성하고, 32°F부터 212°F까지 20도 간격으로 표를 출력하라.result 절을 사용해 실수 x의 세제곱을 돌려주는 함수 cube(x)를 작성하라.n이 짝수면 .true., 홀수면 .false.를 돌려주는 논리형 함수 is_even(n)을 작성하라.swap(a, b)를 작성하고, 호출 전후의 값을 출력해 확인하라.n까지의 합을 되부름으로 구하는 함수 sum_to(n)을 작성하라. recursive와 result를 모두 쓰고, n = 10에 대해 결과가 55인지 확인하라.r을 받아 원의 넓이만 돌려주는 함수 circle_area(r)를 작성하고, r = 1.0, 2.0, 3.0에 대해 출력하라.n = 0부터 15까지 두 결과가 일치하는지 표로 비교하고, 되부름판이 왜 비효율적인지 설명하라.deg2rad(deg)를 작성하라. 이를 이용해 0°부터 360°까지 5° 간격의 사인값을 sine.csv로 출력하고, Python으로 곡선을 그려라.a·x² + b·x + c = 0의 두 실근을 구하는 서브루틴 quadratic_roots(a, b, c, x1, x2, ok)를 작성하라. 판별식이 음수면 논리값 ok를 .false.로 돌려주어 실근이 없음을 알리도록 하라.base와 음이 아닌 정수 지수 n을 받아 거듭제곱을 반복 곱으로 계산하는 함수 power(base, n)을 작성하라(연산자 **를 쓰지 말 것). 같은 값을 base ** n과 비교하라.