Fortran · 기본
계산으로 배우는 현대 Fortran
서브루틴과 함수 - intent 로 의도 밝히기
function 과 subroutine, contains 내부 프로시저, intent(in·out·inout), 가정 형태 배열 인자, pure·elemental, 재귀(recursive)
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장까지는 계산을 모두 main 프로그램 한 곳에 적었다. 계산이 몇 줄일 때는 괜찮지만, 같은 계산을 여러 번 하거나 계산 종류가 늘어나면 한 곳에 모아 둔 코드는 읽기도 고치기도 어려워진다. 이 장에서는 계산을 이름 붙은 묶음으로 떼어 내는 방법을 배운다. 그 묶음을 프로시저(procedure)라 하고, Fortran 에는 값을 하나 돌려주는 function 과 여러 일을 하는 subroutine 두 종류가 있다. 더 중요한 것은 각 프로시저가 인자(argument)를 읽기만 하는지, 결과를 돌려주는지, 고쳐 쓰는지를 코드에 직접 적는 습관이다. 이 의도 표시가 intent 다.
- function 과 subroutine 의 차이를 알고, 상황에 맞게 고른다.
- program 안쪽 contains 아래에 내부 프로시저를 쓰고, 호스트 연관(host association)이 무엇인지 설명한다.
- intent(in), intent(out), intent(inout) 을 구분해 인자에 붙인다.
- 가정 형태 배열 x(:) 로 길이가 다른 배열을 같은 프로시저에 넘긴다.
- pure, elemental, recursive 가 각각 무엇을 약속하는지 알고 쓴다.
문제 상황
작은 기상 관측소에서 8시간 동안 잰 기온과 강수량이 있다. 그런데 이 기온계는 실제보다 0.5도 높게 읽는다는 사실이 확인됐다. 해야 할 일은 다음과 같다.
- 8개 기온 값을 모두 0.5도씩 낮춰 보정한다.
- 보정한 기온의 최저, 최고, 평균을 구한다.
- 섭씨 기온을 화씨로 바꿔 외국 기관에 보낼 표를 만든다.
- 강수량의 합과 비가 온 시간 수를 센다.
이 계산을 main 에 한 줄씩 늘어놓으면 오늘은 돌아간다. 하지만 내일 다른 관측소의 자료가 오면 같은 코드를 복사해야 한다. 복사본 중 하나에서 보정값을 고치고 다른 하나는 잊으면 두 관측소의 결과가 조용히 어긋난다. 계산마다 이름을 붙여 한 곳에 두고 필요한 곳에서 부르면 이 문제가 줄어든다. 더 나아가 "이 계산은 입력을 건드리지 않는다", "이 계산은 입력을 직접 고친다"를 코드에 적어 두면 읽는 사람과 컴파일러가 모두 실수를 일찍 알아챈다.
function 과 subroutine
값을 돌려주느냐, 일을 시키느냐
function 은 수학의 함수와 같다. 인자를 받아 값 하나를 계산해 돌려주고, 그 호출은 식 안에 그대로 들어간다. subroutine 은 값을 돌려주지 않는다. 대신 call 문으로 부르고, 인자를 통해 결과를 여러 개 돌려주거나 인자를 고친다.
| 구분 | 부르는 방법 | 결과를 돌려주는 곳 | 어울리는 일 |
|---|---|---|---|
| function | 식 안에서 이름(인자) | 함수 값 하나 | 평균, 단위 변환처럼 값 하나를 구하는 계산 |
| subroutine | call 이름(인자) | 인자 여러 개 | 최솟값과 최댓값을 함께 구하기, 배열을 고치기 |
이 장의 프로그램에서 평균은 값 하나이므로 function mean_of 로, 최솟값과 최댓값은 두 개이므로 subroutine stats 로 만든다. function 이 돌려줄 값에는 이름을 붙이고 result(m) 처럼 적는다. 함수 몸체 안에서 그 이름 m 에 값을 대입하면 그것이 반환값이다.
contains 아래의 내부 프로시저
프로시저를 어디에 둘지는 다음 장에서 다룰 모듈이 정석이다. 이 장에서는 더 간단한 방법인 내부 프로시저(internal procedure)를 쓴다. program 의 실행문이 끝난 뒤 end program 앞에 contains 한 줄을 쓰고 그 아래에 프로시저를 적는다. 내부 프로시저는 호출하는 쪽의 인터페이스, 곧 인자의 개수와 종류와 intent 를 컴파일러가 이미 알고 있다. 그래서 인자 개수를 틀리거나 종류를 틀리면 컴파일 단계에서 오류가 난다.
내부 프로시저에는 주의할 성질이 하나 있다. 호스트인 program 에서 선언한 변수와 상수가 내부 프로시저 안에서 그대로 보인다. 이것을 호스트 연관이라 한다. 이 장의 코드에서 dp 를 프로시저마다 다시 선언하지 않아도 되는 것도 이 덕분이다. 반대로 프로시저 안에서 지역 변수 선언을 빠뜨리면 같은 이름의 호스트 변수를 쓰게 되어 실수가 숨어 버린다. 프로시저가 쓰는 값은 인자로 받고, 지역 변수는 반드시 프로시저 안에서 선언하는 습관을 들인다.
intent 와 가정 형태 배열
인자의 방향을 적는다
인자는 프로시저에서 쓰는 이름이므로 가인자(dummy argument)라 부른다. 가인자 선언에 intent 를 덧붙이면 그 인자가 값을 어느 방향으로 나르는지 적을 수 있다.
| intent | 들어올 때의 값 | 프로시저 안에서 읽기 | 프로시저 안에서 쓰기 |
|---|---|---|---|
| in | 호출한 쪽의 값 | 가능 | 컴파일 오류 |
| out | 정의되지 않은 값으로 취급 | 쓰기 전에는 의미 없음 | 가능, 반드시 값을 정해야 함 |
| inout | 호출한 쪽의 값 | 가능 | 가능 |
이 장의 correct_bias 에서 bias 는 읽기만 하므로 in, 보정할 배열 x 는 읽고 고치므로 inout 이다. stats 에서 xmin 과 xmax 는 결과를 돌려주는 자리이므로 out 이다. 의도를 적으면 얻는 것이 두 가지 있다. 첫째, intent(in) 인자에 값을 대입하면 컴파일러가 막아 준다. 둘째, 코드를 읽는 사람이 프로시저 본문을 읽지 않고 선언부만 보고도 어떤 인자가 바뀌는지 안다.
가정 형태 배열
배열을 받는 인자는 x(:) 처럼 크기 자리에 콜론만 쓴다. 이것이 가정 형태(assumed-shape) 배열이다. 배열의 크기와 모양은 호출할 때 넘어온 실제 배열에서 정해지고, 프로시저 안에서는 size(x) 로 길이를 묻는다. 그러면 8시간짜리 배열도 24시간짜리 배열도 같은 프로시저로 처리한다. 가정 형태 배열은 호출하는 쪽이 인터페이스를 알아야 하는데, 내부 프로시저는 그 조건을 이미 충족한다. 선언에 x(8) 처럼 숫자를 적는 대신 x(:) 와 size(x) 를 쓰는 것이 이 책의 기본이다.
pure, elemental, recursive
pure: 부작용이 없다는 약속
pure 를 붙인 프로시저는 인자 외에는 아무것도 바꾸지 않겠다고 약속한다. 화면에 출력하지 않고, 호스트 변수를 고치지 않으며, 모든 가인자에 intent 를 적어야 한다. 컴파일러가 이 약속을 검사해 어기면 오류를 낸다. 같은 입력이면 항상 같은 결과가 나오니 읽는 사람도 안심하고 호출 순서를 신경 쓰지 않는다.
elemental: 스칼라 함수를 배열 전체에 적용한다
elemental 은 pure 의 한 종류로, 스칼라(값 하나) 인자로 정의한 프로시저를 배열에도 그대로 쓰게 해 준다. c_to_f 는 실수 하나를 받아 실수 하나를 돌려주도록 썼지만, 배열 temp 를 넘기면 원소마다 적용되어 같은 모양의 배열이 나온다. 앞 장에서 배운 배열 통째 연산과 이어지는 성질이다. 반복문을 쓰지 않아도 되고, 같은 함수를 값 하나에도 배열에도 쓴다.
recursive: 자기 자신을 부르는 프로시저
어떤 계산은 "작은 같은 문제로 줄여서" 풀기 쉽다. 강수량 합은 "첫 값 더하기 나머지의 합"이다. 이렇게 자기 자신을 부르는 프로시저를 재귀(recursion)라 하며 recursive 를 앞에 붙이고 function 은 result 로 반환 이름을 따로 정한다. 재귀에는 더 부르지 않고 끝나는 기저 사례(base case)가 반드시 있어야 한다. 이 장의 total_rain 은 배열 길이가 0이면 0을 돌려주고 끝난다. 길이가 있으면 x(1) 에 x(2:) 의 합을 더한다. x(2:) 는 둘째 원소부터 끝까지의 배열 부분이며, 길이가 1인 배열에서는 길이 0인 배열이 된다.
재귀는 읽기 쉬운 해법을 주지만, 합계 같은 단순한 일은 내장 함수 sum 이 더 짧고 빠르다. 여기서는 재귀의 구조를 익히는 예로 쓰며, 결과를 sum 과 나란히 출력해 같음을 확인한다. 자세한 규칙은 gfortran 문서의 공식 안내에서 확인할 수 있다.
완성 코드
한 파일 main.f90 이다. 다음 명령으로 컴파일한다.
program weather_procs
implicit none
integer, parameter :: dp = kind(1.0d0)
real(dp) :: temp(8), temp_f(8), rain(8)
real(dp) :: tmin, tmax
temp = [12.5_dp, 13.0_dp, 15.5_dp, 18.0_dp, 20.5_dp, 19.0_dp, 16.5_dp, 14.5_dp]
rain = [0.0_dp, 0.5_dp, 2.0_dp, 3.5_dp, 0.0_dp, 1.0_dp, 0.0_dp, 0.5_dp]
print '(a, 8f6.1)', 'raw C :', temp
call correct_bias(temp, 0.5_dp)
print '(a, 8f6.1)', 'fixed C:', temp
temp_f = c_to_f(temp)
print '(a, 8f6.1)', 'fixed F:', temp_f
print '(a, f8.1)', 'boil F :', c_to_f(100.0_dp)
call stats(temp, tmin, tmax)
print '(a, f8.2)', 'min C :', tmin
print '(a, f8.2)', 'max C :', tmax
print '(a, f8.2)', 'mean C :', mean_of(temp)
print '(a, f8.2)', 'rain mm:', total_rain(rain)
print '(a, f8.2)', 'sum mm:', sum(rain)
print '(a, i3)', 'wet hrs:', count_wet(rain, 0.1_dp)
contains
! 기온계 편향을 빼서 배열을 직접 고친다
subroutine correct_bias(x, bias)
real(dp), intent(inout) :: x(:)
real(dp), intent(in) :: bias
x = x - bias
end subroutine correct_bias
! 최솟값과 최댓값을 인자로 돌려준다
subroutine stats(x, xmin, xmax)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: xmin, xmax
xmin = minval(x)
xmax = maxval(x)
end subroutine stats
pure function mean_of(x) result(m)
real(dp), intent(in) :: x(:)
real(dp) :: m
m = sum(x) / real(size(x), dp)
end function mean_of
! 스칼라로 정의했지만 배열에도 쓸 수 있다
elemental function c_to_f(c) result(f)
real(dp), intent(in) :: c
real(dp) :: f
f = 1.8_dp * c + 32.0_dp
end function c_to_f
pure function count_wet(x, threshold) result(k)
real(dp), intent(in) :: x(:), threshold
integer :: k
k = count(x > threshold)
end function count_wet
recursive function total_rain(x) result(s)
real(dp), intent(in) :: x(:)
real(dp) :: s
if (size(x) == 0) then
s = 0.0_dp
else
s = x(1) + total_rain(x(2:))
end if
end function total_rain
end program weather_procs
줄별 해설
선언부. dp 는 앞에서 배운 대로 배정밀도 kind 값이다. temp, temp_f, rain 은 길이 8인 실수 배열이고 tmin, tmax 는 stats 의 결과를 받을 스칼라다. 이 변수들은 호스트 변수이므로 내부 프로시저에서도 보이지만, 아래 프로시저들은 모두 인자로만 값을 받는다.
자료 대입. 배열 생성자 [ ... ] 로 8개 값을 한 번에 넣는다. 모든 상수에 _dp 를 붙여 정밀도를 맞춘다.
보정과 출력. print 의 서식 '(a, 8f6.1)' 은 문자열 하나 뒤에 폭 6, 소수 한 자리인 실수 8개를 찍는다. call correct_bias(temp, 0.5_dp) 가 끝나면 temp 의 8개 원소가 모두 0.5씩 줄어 있다. inout 인자라 호출한 쪽 배열이 바뀐다. 상수 0.5_dp 는 intent(in) 자리에만 넘길 수 있다. inout 이나 out 자리에 상수를 넘기면 고칠 수 없으므로 오류가 난다.
화씨 변환. c_to_f(temp) 는 elemental 함수에 배열을 넘긴 것이라 8개 원소가 모두 변환된 배열을 돌려준다. c_to_f(100.0_dp) 는 같은 함수를 스칼라에 쓴 것이다. 100도의 화씨를 찍어 변환식이 맞는지 확인한다.
통계. call stats(temp, tmin, tmax) 가 최솟값과 최댓값을 한꺼번에 돌려준다. 평균은 값 하나이므로 print 문 안에서 mean_of(temp) 로 바로 부른다. mean_of 안에서 size(x) 는 정수라서 real(size(x), dp) 로 실수로 바꾼 뒤 나눈다.
강수. total_rain(rain) 과 sum(rain) 을 나란히 찍어 재귀 결과가 내장 함수와 같음을 확인한다. count_wet 은 count(x > threshold) 로 기준을 넘는 시간 수를 센다. 비교식 x > threshold 는 논리 배열이 되고, count 가 참인 원소 수를 센다. 서식 i3 은 정수를 폭 3으로 찍는다.
contains 이후. correct_bias 는 x = x - bias 한 줄이 배열 전체에 적용된다. stats 는 두 out 인자에 모두 값을 대입한다. total_rain 은 if 의 두 갈래가 모두 s 에 값을 정한다. 한 갈래라도 정하지 않으면 반환값이 비게 된다.
출력 문자열. 출력 라벨은 모두 영문 8글자로 맞춰 열이 가지런히 보이게 했다.
실행 결과
$ gfortran -std=f2018 -Wall main.f90 -o main
$ ./main
raw C : 12.5 13.0 15.5 18.0 20.5 19.0 16.5 14.5
fixed C: 12.0 12.5 15.0 17.5 20.0 18.5 16.0 14.0
fixed F: 53.6 54.5 59.0 63.5 68.0 65.3 60.8 57.2
boil F : 212.0
min C : 12.00
max C : 20.00
mean C : 15.69
rain mm: 7.50
sum mm: 7.50
wet hrs: 5
보정 후 기온의 합은 125.5 이고 8로 나눈 15.6875 가 서식 f8.2 에 의해 15.69 로 찍힌다. 강수량의 재귀 합과 내장 합이 같고, 비가 온 시간은 0.5, 2.0, 3.5, 1.0, 0.5 의 다섯 시간이다.
실무에서 자주 틀리는 것
intent(in) 인자에 대입한다
읽기 전용으로 선언한 인자를 고치려 하면 컴파일 오류가 난다. 보정 결과를 같은 배열에 돌려주려면 inout 이어야 한다.
! 틀린 코드
subroutine correct_bias(x, bias)
real(dp), intent(in) :: x(:)
real(dp), intent(in) :: bias
x = x - bias ! intent(in) 에 대입: 오류
end subroutine correct_bias
! 고친 코드
subroutine correct_bias(x, bias)
real(dp), intent(inout) :: x(:)
real(dp), intent(in) :: bias
x = x - bias
end subroutine correct_bias
intent(out) 인자 중 일부에 값을 정하지 않는다
out 인자는 호출 시점의 값이 의미 없다. 프로시저가 끝나기 전에 모든 경로에서 값을 정해야 한다. 아래 코드는 xmax 를 정하지 않아 호출한 쪽이 쓰레기 값을 받는다. 컴파일러가 경고하는 경우도 있지만 모든 경로에서 잡아 준다고 기대하면 안 된다.
! 틀린 코드
subroutine stats(x, xmin, xmax)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: xmin, xmax
xmin = minval(x)
end subroutine stats
! 고친 코드
subroutine stats(x, xmin, xmax)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: xmin, xmax
xmin = minval(x)
xmax = maxval(x)
end subroutine stats
재귀에 기저 사례가 없다
멈추는 조건이 없으면 함수는 끝없이 자신을 부르고, 호출이 쌓이다 프로그램이 비정상 종료한다. 길이가 0인 배열까지 내려가도록 첫 분기를 넣는다.
! 틀린 코드
recursive function total_rain(x) result(s)
real(dp), intent(in) :: x(:)
real(dp) :: s
s = x(1) + total_rain(x(2:))
end function total_rain
! 고친 코드
recursive function total_rain(x) result(s)
real(dp), intent(in) :: x(:)
real(dp) :: s
if (size(x) == 0) then
s = 0.0_dp
else
s = x(1) + total_rain(x(2:))
end if
end function total_rain
pure 프로시저 안에서 출력한다
화면 출력은 부작용이므로 pure 안에서는 쓸 수 없고 컴파일 오류가 난다. 디버깅 출력이 필요하면 값을 돌려주고 호출하는 쪽에서 찍는다.
! 틀린 코드
pure function mean_of(x) result(m)
real(dp), intent(in) :: x(:)
real(dp) :: m
m = sum(x) / real(size(x), dp)
print '(f8.2)', m ! pure 안의 출력: 오류
end function mean_of
! 고친 코드
pure function mean_of(x) result(m)
real(dp), intent(in) :: x(:)
real(dp) :: m
m = sum(x) / real(size(x), dp)
end function mean_of
! 출력은 호출하는 쪽에서: print '(f8.2)', mean_of(temp)
한눈에 보기
| 키워드 | 뜻 | 쓰는 때 |
|---|---|---|
| function ... result(r) | 값 하나를 돌려주는 프로시저 | 평균, 단위 변환 |
| subroutine / call | 인자로 결과를 돌려주거나 고치는 프로시저 | 최솟값과 최댓값 동시 계산 |
| contains | 그 아래에 내부 프로시저를 둔다 | 작은 프로그램을 나눌 때 |
| intent(in) | 읽기만 한다 | 입력값, 기준값 |
| intent(out) | 결과를 반드시 정해서 돌려준다 | 계산 결과 인자 |
| intent(inout) | 읽고 고친다 | 배열 보정 |
| x(:) | 호출 시 크기가 정해지는 배열 인자 | 길이가 달라지는 자료 |
| pure | 부작용이 없다는 약속 | 순수한 계산 |
| elemental | 스칼라 정의를 배열에 원소별 적용 | 단위 변환 |
| recursive | 자기 자신을 부를 수 있다 | 작은 같은 문제로 줄어드는 계산 |
연습 문제
- function value_range(x) 를 pure 로 만들어, 배열의 최댓값에서 최솟값을 뺀 값을 돌려주게 하라. 보정한 기온 배열의 일교차를 f8.2 로 출력하라.
- 화씨를 섭씨로 바꾸는 elemental 함수 f_to_c 를 만들고, 값 하나와 배열 temp_f 에 각각 적용하라. 변환식은 (f - 32) / 1.8 이다.
- recursive function count_dry(x) 를 만들어 강수량이 0.0 인 시간 수를 재귀로 세라. 기저 사례는 길이 0이다.
- 다음 세 인자에 어떤 intent 를 붙여야 하는지 고르고 이유를 한 문장씩 써라. (가) 강수량을 모두 0으로 되돌리는 reset_rain(x) 의 x, (나) 기준 강수량 threshold, (다) stats 가 돌려주는 xmin.
정답과 해설
1번. 일교차는 값 하나이므로 function 이 어울린다.
pure function value_range(x) result(r)
real(dp), intent(in) :: x(:)
real(dp) :: r
r = maxval(x) - minval(x)
end function value_range
! main 에서
print '(a, f8.2)', 'range :', value_range(temp)
보정한 기온의 최댓값 20.0 과 최솟값 12.0 을 쓰면 8.00 이 출력된다.
2번. 정의는 스칼라로 쓰고 elemental 을 붙이면 된다.
elemental function f_to_c(f) result(c)
real(dp), intent(in) :: f
real(dp) :: c
c = (f - 32.0_dp) / 1.8_dp
end function f_to_c
! main 에서
print '(a, f8.1)', 'one C :', f_to_c(212.0_dp)
print '(a, 8f6.1)', 'all C :', f_to_c(temp_f)
212도는 100.0 으로, temp_f 는 소수 첫째 자리까지 원래의 섭씨 기온으로 돌아온다.
3번. total_rain 과 같은 모양이며 더하는 값만 달라진다.
recursive function count_dry(x) result(k)
real(dp), intent(in) :: x(:)
integer :: k
if (size(x) == 0) then
k = 0
else if (x(1) == 0.0_dp) then
k = 1 + count_dry(x(2:))
else
k = count_dry(x(2:))
end if
end function count_dry
이 장의 강수 자료에서는 3이다. 실수를 == 로 비교하는 것은 앞에서 배운 오차 문제가 있지만, 여기서는 0.0 을 직접 대입한 값이라 정확히 같다. 측정값을 계산해 얻은 실수라면 허용 오차를 두고 비교해야 한다.
4번. (가) inout 보다 out 이 어울린다. 이전 값을 읽지 않고 전부 새로 정하기 때문이다. 단, 배열 크기는 호출한 쪽 배열에서 오므로 x(:) 로 받는다. (나) in 이다. 기준값은 읽기만 한다. (다) out 이다. 호출 전 값은 필요 없고 결과를 돌려주는 자리이기 때문이다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.