Devin.KR

파생 타입 - 관측값을 한 묶음으로

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서는 관측 자료를 파일에서 읽어 서식에 맞춰 출력하는 방법을 다뤘다. 그 과정에서 시각, 기온, 강수량이 서로 다른 배열에 따로 담겼다. 값이 서너 개일 때는 이것으로 충분하지만, 항목이 늘어나면 "같은 시각에 속한 값"이라는 관계를 프로그래머가 머릿속으로 계속 기억해야 한다. 이 장에서는 관련된 값을 이름 붙은 한 덩어리로 묶는 파생 타입(derived type)을 배운다. 타입에 그 값을 다루는 함수까지 붙이는 방법도 함께 다룬다.

  • type 으로 시각·기온·강수량을 묶은 타입을 정의하고 % 로 구성 요소(component)를 읽고 쓴다.
  • 파생 타입 배열을 만들고, obs(i)%temp_c 와 obs%temp_c 의 차이를 구별한다.
  • contains 와 procedure 로 타입에 속한 프로시저(타입 바운드 프로시저)를 선언하고 obs%show() 형태로 호출한다.
  • 값을 검증하면서 객체를 만드는 생성 함수의 관례를 익힌다.

문제 상황

관측소에서 오전 6시부터 오후 1시까지 여덟 시간의 기온과 강수량을 받았다고 하자. 지금까지 방식대로라면 hours(8), temps(8), rains(8) 세 배열을 만든다. 이 방식은 곧 문제를 일으킨다.

첫째, 서브루틴에 자료를 넘길 때마다 인수가 세 개씩 따라간다. 풍속이나 기압을 추가하면 모든 호출부를 고쳐야 한다. 둘째, 자료를 정렬하거나 일부만 골라낼 때 세 배열을 똑같이 재배열해야 하는데, 하나라도 빠뜨리면 14시의 기온에 13시의 강수량이 붙는 오류가 생긴다. 컴파일러는 이 오류를 알려 주지 않는다. 셋째, "비가 왔는가"를 판단하는 기준(예: 0.1 mm 이상)이 프로그램 곳곳에 흩어져 서로 어긋나기 쉽다.

이런 문제는 시각, 기온, 강수량을 하나의 관측값으로 묶고, 관측값에 대한 질문("비가 왔는가", "화씨로는 몇 도인가")을 그 타입 가까이에 두면 줄어든다. 이 장의 프로그램은 여덟 개의 관측값을 만들어 표로 출력하고 평균 기온, 총강수량, 비 온 시간 수, 최고 기온 시각을 계산한다. 자료는 파일에서 읽지 않고 코드에 직접 적는다. 파일 읽기는 앞 장의 방법으로 바꿔 끼울 수 있다.

파생 타입의 정의와 사용

type 으로 구성 요소 묶기

파생 타입은 프로그래머가 정의하는 자료형이다. integer, real 같은 내장 타입을 재료로 삼아 새 타입을 만든다. 이 장의 타입은 다음과 같다.

type :: observation
  integer  :: hour    = 0
  real(wp) :: temp_c  = 0.0_wp
  real(wp) :: rain_mm = 0.0_wp
end type observation

hour, temp_c, rain_mm 을 구성 요소라 한다. 구성 요소마다 = 0 처럼 기본값을 줄 수 있다. 이 타입의 변수를 선언하면 따로 대입하지 않아도 기본값이 들어 있으므로, 초기화하지 않은 쓰레기 값을 읽는 실수를 막아 준다. 여기서 wp 는 두 장 앞에서 배운 대로 정밀도를 고르는 종류 값(kind)이며, 이 장에서는 8바이트 실수인 real64 로 정했다.

타입을 정의한 것만으로는 메모리에 아무것도 생기지 않는다. 타입은 설계도이고 type(observation) :: latest 처럼 변수를 선언해야 실제 값이 생긴다. 변수의 구성 요소는 % 기호로 가리킨다.

latest%temp_c = 21.4_wp
print *, latest%hour

다른 언어에서 점(.)을 쓰던 자리에 Fortran 은 % 를 쓴다. 점은 .and. 같은 연산자 표기와 겹치기 때문이다.

값을 한꺼번에 채우는 구조 생성자(structure constructor)도 있다. 타입 이름을 함수처럼 쓰고 구성 요소를 선언 순서대로 적는다. 예를 들면 observation(12, 22.0_wp, 2.6_wp) 이다. 같은 타입의 변수끼리는 b = a 로 통째로 복사된다. 구성 요소마다 대입문을 쓸 필요가 없다.

파생 타입 배열

파생 타입도 배열의 원소 타입이 될 수 있다. type(observation) :: obs(8) 은 관측값 여덟 개를 담는다. 이때 구성 요소를 가리키는 방법은 두 가지다. 어느 쪽을 쓰느냐에 따라 결과의 모양이 달라진다.

배열 원소와 구성 요소를 가리키는 표현의 결과
표현가리키는 것결과의 모양
obs(3)세 번째 관측값 전체observation 하나
obs(3)%temp_c세 번째 관측값의 기온실수 하나
obs%temp_c모든 관측값의 기온실수 배열(길이 8)
obs(2:4)%rain_mm2~4번째 관측값의 강수량실수 배열(길이 3)

세 번째 줄이 중요하다. obs%temp_c 는 배열이므로 앞서 배운 배열 연산을 그대로 쓸 수 있다. sum(obs%temp_c) 는 기온의 합이고 maxloc(obs%temp_c, dim=1) 은 기온이 가장 높은 원소의 번호다. 반복문을 쓰지 않고 한 항목만 골라 계산할 수 있다.

파생 타입 배열에서 obs(2)%temp_c 는 값 하나이고 obs%temp_c 는 모든 원소의 기온을 모은 배열이다.

그림은 논리적인 구조를 보여 준다. 각 행이 관측값 하나이고, 강조한 열이 obs%temp_c 가 가리키는 값들이다. 이 값들이 메모리에서 어떻게 놓이는지는 컴파일러가 정한다. 프로그램은 그 배치에 기대지 않고 % 표기로만 접근한다.

타입에 붙는 프로시저

contains 와 procedure

"비가 왔는가"를 판단하는 함수는 관측값에만 쓰인다. 그렇다면 이 함수를 타입의 일부로 선언하는 편이 자연스럽다. 타입 정의 안에 contains 를 쓰고 그 아래에 procedure 문을 나열한다.

type :: observation
  integer  :: hour    = 0
  real(wp) :: temp_c  = 0.0_wp
  real(wp) :: rain_mm = 0.0_wp
contains
  procedure :: is_rainy
  procedure :: temp_f
  procedure :: show
end type observation

procedure :: is_rainy 는 "이 타입에는 is_rainy 라는 프로시저가 속한다"는 선언이다. 실제 본문은 모듈의 contains 아래에 따로 쓴다. 이 두 contains 는 서로 다르다. 타입 안의 것은 타입에 속한 프로시저의 목록을 열고, 모듈 안의 것은 모듈 프로시저의 본문을 연다.

본문에서 첫 번째 인수는 호출한 객체 자신을 받는다. 이 인수를 class(observation) 로 선언하며, 이 책에서는 이름을 self 로 쓴다. 이름은 규칙이 아니라 관례다.

pure function is_rainy(self) result(rainy)
  class(observation), intent(in) :: self
  logical :: rainy

  rainy = self%rain_mm >= rain_threshold
end function is_rainy

class 는 이 타입과 이 타입에서 확장된 타입을 모두 받겠다는 뜻이다. 이 책에서는 확장 타입을 다루지 않으므로 "타입 바운드 프로시저의 첫 인수는 type 이 아니라 class 로 쓴다" 정도로 기억하면 된다. 호출은 다음과 같이 한다.

if (obs(3)%is_rainy()) print *, 'rain'
call obs(3)%show()

점 앞의 obs(3) 이 자동으로 첫 인수 self 가 된다. 괄호 안에는 나머지 인수만 적는다. 인수가 없으면 빈 괄호를 쓴다. 함수 호출이라는 사실을 드러내려면 빈 괄호가 필요하다.

obs(3)%show() 호출에서 점 앞의 obs(3) 이 첫 인수 self 로 전달된다.

모듈이 필요한 이유

타입 바운드 프로시저로 연결할 수 있는 것은 모듈 프로시저, 또는 인터페이스가 명시된 외부 프로시저다. 프로그램 안의 contains 에 쓴 내부 프로시저는 연결할 수 없다. 그래서 타입과 그 프로시저는 모듈에 함께 둔다. 앞서 모듈을 파일로 나누는 방법을 익혔으므로 이 장은 obs_mod.f90 와 main.f90 두 파일로 구성한다. 모듈에는 private 를 선언해 내부 상수 rain_threshold 를 숨기고, 밖에서 쓸 이름만 public 으로 연다.

생성 함수 관례

구조 생성자는 편리하지만 값을 검사하지 않는다. 시각이 25이거나 강수량이 음수인 관측값도 그대로 만들어진다. 이런 값은 처음부터 막아야 이후 계산이 틀어지지 않는다. 관례는 객체를 만들어 돌려주는 생성 함수(constructor function)를 따로 두는 것이다. 이 책은 make_ 접두어를 붙인다.

function make_observation(hour, temp_c, rain_mm) result(obs)
  integer, intent(in)  :: hour
  real(wp), intent(in) :: temp_c, rain_mm
  type(observation)    :: obs

  if (hour < 0 .or. hour > 23) error stop 'make_observation: hour must be 0-23'
  if (rain_mm < 0.0_wp) error stop 'make_observation: rain_mm must not be negative'
  obs = observation(hour, temp_c, rain_mm)
end function make_observation

잘못된 값이 들어오면 error stop 으로 프로그램을 즉시 멈춘다. 마지막 줄에서 구조 생성자로 실제 값을 채운다. 규모가 큰 프로그램에서는 함수 이름을 타입 이름과 같게 하여 구조 생성자를 대신하는 방법도 쓴다. 그러려면 제네릭 인터페이스를 선언해야 하는데, 이 장에서는 이름이 분명한 make_ 방식을 택한다.

완성 코드

모듈 파일을 먼저 컴파일하고 main.f90 을 마지막에 컴파일한다. 컴파일러가 모듈을 처리하면서 만드는 .mod 파일을 main.f90 이 읽기 때문이다.

obs_mod.f90

module obs_mod
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: wp, observation, make_observation

  integer, parameter :: wp = real64
  real(wp), parameter :: rain_threshold = 0.1_wp

  type :: observation
    integer  :: hour    = 0
    real(wp) :: temp_c  = 0.0_wp
    real(wp) :: rain_mm = 0.0_wp
  contains
    procedure :: is_rainy
    procedure :: temp_f
    procedure :: show
  end type observation

contains

  function make_observation(hour, temp_c, rain_mm) result(obs)
    integer, intent(in)  :: hour
    real(wp), intent(in) :: temp_c, rain_mm
    type(observation)    :: obs

    if (hour < 0 .or. hour > 23) error stop 'make_observation: hour must be 0-23'
    if (rain_mm < 0.0_wp) error stop 'make_observation: rain_mm must not be negative'
    obs = observation(hour, temp_c, rain_mm)
  end function make_observation

  pure function is_rainy(self) result(rainy)
    class(observation), intent(in) :: self
    logical :: rainy

    rainy = self%rain_mm >= rain_threshold
  end function is_rainy

  pure function temp_f(self) result(f)
    class(observation), intent(in) :: self
    real(wp) :: f

    f = self%temp_c * 9.0_wp / 5.0_wp + 32.0_wp
  end function temp_f

  subroutine show(self)
    class(observation), intent(in) :: self
    character(len=4) :: state

    state = merge('rain', 'dry ', self%is_rainy())
    write (*, '(i4, ":00", 3f8.1, 3x, a)') self%hour, self%temp_c, self%temp_f(), &
                                           self%rain_mm, trim(state)
  end subroutine show

end module obs_mod

main.f90

program station_summary
  use obs_mod, only: wp, observation, make_observation
  implicit none

  integer, parameter :: n = 8
  type(observation)  :: obs(n)
  type(observation)  :: latest
  integer  :: i, rainy_hours, peak
  real(wp) :: mean_temp, total_rain

  obs = [ make_observation(6,  14.2_wp, 0.0_wp), &
          make_observation(7,  15.0_wp, 0.0_wp), &
          make_observation(8,  16.8_wp, 0.4_wp), &
          make_observation(9,  18.1_wp, 1.2_wp), &
          make_observation(10, 19.5_wp, 0.0_wp), &
          make_observation(11, 21.3_wp, 0.0_wp), &
          make_observation(12, 22.0_wp, 2.6_wp), &
          make_observation(13, 20.4_wp, 0.8_wp) ]

  write (*, '(a)') '   hour  temp_C  temp_F rain_mm   state'
  do i = 1, n
    call obs(i)%show()
  end do

  mean_temp  = sum(obs%temp_c) / n
  total_rain = sum(obs%rain_mm)
  peak       = maxloc(obs%temp_c, dim=1)

  rainy_hours = 0
  do i = 1, n
    if (obs(i)%is_rainy()) rainy_hours = rainy_hours + 1
  end do

  write (*, '(a, f8.2)') 'mean temp (C):', mean_temp
  write (*, '(a, f8.1)') 'total rain (mm):', total_rain
  write (*, '(a, i3)')   'rainy hours:', rainy_hours
  write (*, '(a, i3, a, f5.1)') 'peak:', obs(peak)%hour, ':00,', obs(peak)%temp_c

  latest = obs(n)
  latest%temp_c = latest%temp_c + 1.0_wp
  write (*, '(a, f5.1, a, f5.1)') 'copy:', latest%temp_c, ' original:', obs(n)%temp_c
end program station_summary

줄별 해설

obs_mod.f90

  • 2행: 표준 모듈 iso_fortran_env 에서 8바이트 실수 종류 값 real64 만 가져온다.
  • 4~5행: private 로 모듈의 이름을 기본적으로 숨기고, 밖에서 쓸 wp, observation, make_observation 만 연다. rain_threshold 와 is_rainy 등의 본문 이름은 숨겨져 있어도 타입을 통한 호출은 가능하다.
  • 7~8행: 종류 값 wp 와 비 온 것으로 치는 기준 0.1 mm 를 상수로 선언한다. 기준을 바꿀 때 이 한 줄만 고치면 된다.
  • 10~13행: 구성 요소 세 개와 기본값이 있는 타입 정의다.
  • 14~17행: contains 아래에서 타입에 속한 프로시저 세 개를 선언한다. 본문은 모듈의 contains 아래에 쓴다.
  • 22~30행: 생성 함수다. 27~28행에서 범위를 벗어난 시각과 음수 강수량을 거르고, 29행에서 구조 생성자로 값을 채운다. 결과 변수 이름 obs 는 이 함수 안에서만 쓰인다.
  • 32~37행: pure 는 부작용이 없는 함수라는 표시다. self 는 intent(in) 이라 읽기만 한다.
  • 39~44행: 섭씨를 화씨로 바꾼다. 9.0_wp / 5.0_wp 처럼 실수로 써서 정수 나눗셈이 되지 않게 했다.
  • 50행: merge 는 조건이 참이면 첫 값을, 거짓이면 둘째 값을 고른다. 두 문자열의 길이를 4로 맞춰야 하므로 'dry ' 뒤에 공백을 하나 넣었다.
  • 51~52행: 서식 3f8.1 은 실수 세 개를 폭 8, 소수 한 자리로 쓴다. 줄 끝의 & 는 다음 줄로 이어진다는 표시이고, trim(state) 가 끝의 공백을 잘라 낸다.

main.f90

  • 2행: 모듈에서 쓸 이름만 가져온다. 빠뜨린 이름을 쓰면 컴파일 오류가 되어 어디서 왔는지 분명하다.
  • 6~7행: 관측값 8개짜리 배열과 하나짜리 변수를 선언한다. 기본값 덕분에 대입 전에도 안전한 값이 들어 있다.
  • 11~18행: 생성 함수를 여덟 번 불러 만든 배열을 대괄호 배열 생성자로 묶어 obs 에 한 번에 대입한다.
  • 21~23행: 원소를 하나씩 꺼내 show 를 호출한다. 호출한 obs(i) 가 show 의 self 가 된다.
  • 25~27행: 반복문 없이 obs%temp_c 와 obs%rain_mm 배열에 합계 함수를 적용한다. maxloc 은 최댓값의 위치(1부터 센 번호)를 돌려주므로 시각 12시의 번호는 7이다.
  • 29~32행: 비 온 시간을 센다. 비 판정 기준은 모듈 안에만 있고 여기서는 is_rainy 만 부른다.
  • 37행: 번호 peak 로 최고 기온 관측값의 시각과 기온을 꺼낸다.
  • 39~41행: latest = obs(n) 은 값을 복사한다. 복사본의 기온을 1도 올려도 원본은 그대로임을 출력으로 확인한다.

실행 결과

$ gfortran -std=f2018 -Wall obs_mod.f90 main.f90 -o station
$ ./station
   hour  temp_C  temp_F rain_mm   state
   6:00    14.2    57.6     0.0   dry
   7:00    15.0    59.0     0.0   dry
   8:00    16.8    62.2     0.4   rain
   9:00    18.1    64.6     1.2   rain
  10:00    19.5    67.1     0.0   dry
  11:00    21.3    70.3     0.0   dry
  12:00    22.0    71.6     2.6   rain
  13:00    20.4    68.7     0.8   rain
mean temp (C):   18.41
total rain (mm):     5.0
rainy hours:  4
peak: 12:00, 22.0
copy: 21.4 original: 20.4

평균 기온은 합 147.3 을 8로 나눈 18.4125 를 소수 둘째 자리로 쓴 값이다. 비 온 시간은 8, 9, 12, 13시의 네 시간이다.

실무에서 자주 틀리는 것

생성자 인수의 순서를 바꿔 쓴다

기온과 강수량은 둘 다 실수이므로 순서를 바꿔 써도 컴파일러가 잡지 못한다.

obs(3) = make_observation(8, 0.4_wp, 16.8_wp)   ! 기온 0.4, 강수 16.8 이 된다

인수 이름을 붙여 호출하면 순서가 틀려도 뜻이 변하지 않는다.

obs(3) = make_observation(hour=8, temp_c=16.8_wp, rain_mm=0.4_wp)

첫 인수를 type 으로 선언한다

타입 바운드 프로시저의 객체 인수는 class 여야 한다. type 으로 쓰면 컴파일 오류가 난다.

function temp_f(self) result(f)
  type(observation), intent(in) :: self   ! 오류
function temp_f(self) result(f)
  class(observation), intent(in) :: self

main.f90 을 먼저 컴파일한다

main.f90 이 use obs_mod 를 하는 시점에 obs_mod.mod 가 없으면 컴파일러가 모듈을 찾지 못해 오류를 낸다.

$ gfortran -std=f2018 -Wall main.f90 obs_mod.f90 -o station   ! 모듈이 뒤에 있어 실패
$ gfortran -std=f2018 -Wall obs_mod.f90 main.f90 -o station

복사본을 고쳐 놓고 원본이 바뀌길 기대한다

파생 타입의 대입은 값을 복사한다. 복사본을 고쳐도 배열 원소는 바뀌지 않는다.

latest = obs(n)
latest%temp_c = 25.0_wp     ! obs(n) 은 그대로

원본을 고치려면 원소를 직접 지정하거나 associate 로 별명을 붙인다.

associate (o => obs(n))
  o%temp_c = 25.0_wp        ! obs(n)%temp_c 가 바뀐다
end associate

한눈에 보기

이 장에서 쓴 문법과 주의점
주제문법예주의
타입 정의type :: 이름 … end typetype :: observation정의만으로는 변수가 생기지 않는다
구성 요소 접근변수%요소latest%temp_c점이 아니라 % 를 쓴다
배열의 한 항목배열%요소sum(obs%temp_c)결과가 배열이다
타입 바운드 선언contains + procedure ::procedure :: show모듈 프로시저만 연결한다
타입 바운드 호출객체%이름(인수)obs(i)%show()객체가 첫 인수로 자동 전달된다
객체 인수class(타입)class(observation) :: selftype 으로 쓰면 오류
생성 함수make_이름 함수make_observation(…)범위를 검사한다
흩어진 배열과 파생 타입 배열의 비교
비교 항목배열 세 개파생 타입 배열
서브루틴에 넘길 인수배열마다 하나씩배열 하나
항목을 추가할 때모든 호출부 수정타입 정의만 수정
재배열할 때 어긋날 위험있다없다
판정 기준의 위치여러 곳에 분산타입의 프로시저 한 곳

연습 문제

  1. 관측값에 풍속 wind_ms (m/s, 기본값 0.0)를 추가하려 한다. 타입 정의를 어떻게 고쳐야 기존의 observation(hour, temp, rain) 형태 호출이 계속 동작하는지 설명하라.
  2. 기온이 기준값 이상인지 알려 주는 타입 바운드 함수 is_hot(self, limit) 를 작성하라. 이 장의 자료에서 기준값을 21.0 으로 하면 몇 시간이 해당하는가?
  3. 비가 온 시간들만의 시간당 평균 강수량을 배열 구성 요소와 mask 를 써서 한 줄로 계산하는 식을 쓰고, 이 장의 자료에서의 값을 구하라.
  4. 타입 바운드 프로시저의 본문을 main.f90 의 contains 아래에 두면 안 되는 이유를 설명하라.

정답과 해설

  1. 새 구성 요소를 기존 구성 요소 뒤에 추가한다. 구조 생성자는 선언 순서대로 값을 받고, 기본값이 있는 요소는 생략할 수 있다.
    real(wp) :: wind_ms = 0.0_wp   ! rain_mm 다음 줄에 추가
    중간에 넣으면 기존 호출의 인수가 한 칸씩 밀려 뜻이 달라지므로 주의한다. 생성 함수에 풍속을 받으려면 optional 인수로 추가하면 기존 호출도 유지된다.
  2. 모듈에 procedure :: is_hot 을 추가하고 본문을 쓴다.
    pure function is_hot(self, limit) result(hot)
      class(observation), intent(in) :: self
      real(wp), intent(in) :: limit
      logical :: hot
    
      hot = self%temp_c >= limit
    end function is_hot
    호출은 obs(7)%is_hot(21.0_wp) 처럼 한다. 21.0 이상인 값은 21.3(11시)과 22.0(12시)이므로 2시간이다. 20.4 는 해당하지 않는다.
  3. mean_rain = sum(obs%rain_mm, mask = obs%rain_mm >= 0.1_wp) / count(obs%rain_mm >= 0.1_wp)
    비 온 시간의 강수량 합은 0.4 + 1.2 + 2.6 + 0.8 = 5.0 이고 시간 수는 4이므로 값은 1.25 mm 다. count 는 조건이 참인 원소의 개수를 정수로 돌려준다.
  4. 타입 바운드 프로시저로 연결할 수 있는 것은 모듈 프로시저나 인터페이스가 명시된 외부 프로시저다. 프로그램의 contains 아래에 쓴 내부 프로시저는 연결 대상이 아니다. 타입 정의가 모듈에 있으므로 프로시저 본문도 같은 모듈에 두는 것이 가장 자연스럽다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.