Fortran · 심화
객체와 수치 해석으로 깊어지는 Fortran
추상 타입과 다형성
abstract type 과 deferred 프로시저, abstract interface, select type, class(*) 무제한 다형성
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 형식 바인딩 프로시저로 데이터와 동작을 하나의 타입에 묶었다. 금속판 객체가 온도 배열을 보관하고 자신의 상태를 출력하도록 만들면, 호출하는 쪽에서 배열의 내부 구조를 일일이 알 필요가 줄어든다. 이번에는 같은 이름으로 호출하되 실제 동작은 객체의 종류에 따라 달라지는 설계를 다룬다.
2차원 금속판의 가장자리를 일정한 온도로 유지하는 조건과, 가장자리 바깥으로 열이 빠져나가지 않는 조건은 서로 다른 계산을 요구한다. 그러나 호출하는 쪽에서 필요한 요청은 둘 다 “가장자리 값을 설정하라”다. 추상 타입(abstract type)은 이 공통 요청을 선언하고, 구체적인 자식 타입은 각자의 처리 방법을 제공한다. 다형성(polymorphism)은 호출 시점에 실제 객체의 종류에 맞는 동작을 선택하게 한다.
- 직접 객체를 만들 수 없는 추상 타입으로 공통 계약을 정의한다.
- 지연 바인딩(deferred binding)과 추상 인터페이스(abstract interface)로 자식 타입이 구현할 프로시저의 형식을 정한다.
class로 받은 객체의 형식 바인딩 프로시저를 호출하고, 필요한 곳에서select type으로 실제 타입을 구별한다.class(*)무제한 다형성(unlimited polymorphism)의 용도와 제약을 이해한다.
문제 상황
열 확산 시뮬레이터에서 경계 조건을 문자열로 관리한다고 가정한다. 가장자리를 설정하는 곳마다 if (mode == 'fixed') 같은 분기가 나타난다. 온도를 저장하는 변수도 있고, 단열 여부를 저장하는 변수도 있다. 새 조건을 추가하면 관련된 분기와 변수 조합을 함께 확인해야 한다. 조건 이름은 단열인데 고정 온도 값이 전달되는 식의 모순도 생길 수 있다.
여기서 필요한 분리는 조건을 선택하는 일과 선택된 조건을 실행하는 일이다. 프로그램의 시작 부분에서는 구체적인 조건 객체를 만든다. 계산을 수행하는 부분에서는 공통 부모 타입으로 객체를 받아 apply를 호출한다. 고정 온도를 적용할지, 이웃 값을 복사할지는 객체가 결정한다.
이번 예제는 시간에 따른 열 확산을 아직 계산하지 않는다. 작은 온도 배열을 준비하고 두 경계 조건을 각각 한 번 적용한다. 이렇게 범위를 줄이면 수치 계산식보다 타입의 관계와 호출 규칙에 집중할 수 있다. 이후 온도를 갱신하는 계산이 추가되어도 경계 조건을 호출하는 문장은 그대로 사용할 수 있다.
배열의 첫 번째 첨자는 가로 위치, 두 번째 첨자는 세로 위치로 정한다. 배열 크기는 가로 다섯 칸, 세로 네 칸이다. 기본 온도는 20이고, 내부의 두 칸만 각각 80과 60으로 설정한다. 두 조건은 같은 초기 배열에서 출발하므로 결과 차이는 경계 처리에서만 발생한다.
추상 타입은 동작의 계약을 선언한다
추상 타입은 여러 구체 타입이 공유할 이름과 규칙을 담는다. 선언에 abstract를 붙이면 그 타입 자체의 객체를 만들 수 없다. 아직 처리 방법을 제공하지 않은 공통 개념을 실제 계산 객체처럼 사용하는 일을 막는 것이다. 추상 타입에도 데이터 성분과 구현된 바인딩을 둘 수 있지만, 이번 예제에서는 경계 조건의 공통 동작만 선언한다.
type, abstract :: boundary_t
contains
procedure(apply_interface), deferred :: apply
end type boundary_t
apply는 객체를 통해 호출할 바인딩 이름이다. apply_interface는 호출에 필요한 인수 형식을 정하는 인터페이스 이름이다. deferred는 이 타입에서 실제 구현을 제공하지 않고 자식 타입에 구현을 맡긴다는 뜻이다. 이 선언만으로 실행할 프로시저가 만들어지는 것은 아니다.
구체적인 자식 타입은 extends(boundary_t)로 부모와의 관계를 선언하고, 같은 바인딩 이름을 실제 프로시저에 연결한다. 추상으로 선언되지 않은 자식 타입은 상속받은 지연 바인딩을 구현해야 한다. 구현하지 않고 객체를 만들려 하면 컴파일 단계에서 문제가 드러난다.
type, extends(boundary_t) :: fixed_boundary_t
real(real64) :: temperature = 10.0_real64
contains
procedure :: apply => apply_fixed
end type fixed_boundary_t
이 선언은 “고정 온도 경계도 경계 조건의 한 종류이며, 자신의 적용 방법은 apply_fixed다”라는 관계를 표현한다. 호출하는 쪽에서는 apply_fixed라는 구현 이름을 알 필요가 없다. 객체의 apply를 호출하면 된다.
추상 인터페이스가 맞추는 것
추상 인터페이스는 구현 프로시저가 따를 인수 목록을 기술한다. 예제의 경계 적용에는 경계 객체와 온도 배열이 필요하다. 객체 설정은 읽기만 하고 배열은 변경하므로 각각 intent(in), intent(inout)을 사용한다.
abstract interface
subroutine apply_interface(self, temperature)
import :: boundary_t, real64
class(boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
end subroutine apply_interface
end interface
인터페이스 본문은 별도의 범위이므로 바깥에서 선언한 타입과 종류 상수를 import로 가져온다. 여기서 use로 자신이 속한 모듈을 다시 불러오는 구조를 만들 필요는 없다. import는 인터페이스가 형식을 설명하는 데 필요한 이름을 보이게 한다.
기본 설정에서 첫 번째 더미 인수는 전달 객체 인수(passed-object dummy argument)다. condition%apply(temperature)라고 호출하면 condition이 self에 자동으로 전달된다. 따라서 괄호 안에 객체를 한 번 더 적지 않는다. 확장 가능한 타입의 전달 객체 인수는 class로 선언한다.
자식 구현에서 전달 객체 인수는 해당 자식 타입으로 바뀐다. 고정 온도 구현은 class(fixed_boundary_t), 단열 구현은 class(insulated_boundary_t)를 사용한다. 나머지 인수의 타입, 종류, 차원 수, 의도 같은 특성은 부모 바인딩의 인터페이스와 호환되어야 한다. 바인딩을 재정의했다고 해서 배열 인수를 정수 스칼라로 바꾸어도 되는 것은 아니다.
| 구성 요소 | 예제 이름 | 정하는 내용 |
|---|---|---|
| 추상 타입 | boundary_t | 경계 조건이라는 공통 범주 |
| 지연 바인딩 | apply | 자식 타입이 제공할 동작 이름 |
| 추상 인터페이스 | apply_interface | 동작을 호출할 때의 인수 형식 |
| 구체 바인딩 | apply_fixed | 고정 온도 조건의 실제 처리 |
같은 호출과 서로 다른 구현
class(boundary_t)로 선언한 더미 인수는 부모 타입뿐 아니라 그 확장 타입의 객체도 받을 수 있다. 선언에 쓰인 타입을 선언 타입(declared type), 실행 중 실제 객체의 타입을 동적 타입(dynamic type)이라 한다. 고정 온도 객체를 전달하면 선언 타입은 boundary_t지만 동적 타입은 fixed_boundary_t다.
이 상태에서 condition%apply(temperature)를 실행하면 동적 타입에 연결된 구현이 호출된다. 단열 객체를 전달한 다음에도 같은 문장을 사용하지만 실행되는 프로시저는 달라진다. 이러한 선택은 형식 바인딩 호출이 수행한다. 호출하기 전에 매번 select type으로 구현 이름을 골라줄 필요가 없다.
완성 코드에서는 class(boundary_t), allocatable 변수를 사용한다. 이 변수에 고정 온도 타입으로 할당한 객체를 넣고 계산한 뒤 해제한다. 이어서 단열 타입으로 새 객체를 할당한다. 선언 타입은 그대로지만 두 번째 할당 후에는 동적 타입이 바뀐다. 이미 할당된 변수를 다른 타입으로 다시 할당하려면 먼저 기존 할당을 해제해야 한다.
타입을 구별할 때와 타입을 열어 둘 때
공통 동작은 부모 타입의 바인딩으로 호출하는 편이 자연스럽다. 반면 화면에 고정 온도 값을 표시하려면 자식 타입에만 있는 temperature 성분을 읽어야 한다. 부모 타입에는 그 성분이 없으므로 공통 선언만으로 접근할 수 없다. 이런 제한된 용도에 select type을 사용한다.
select type (condition)
type is (fixed_boundary_t)
print *, condition%temperature
type is (insulated_boundary_t)
print *, condition%enabled
class default
print *, 'unknown boundary'
end select
위 조각의 출력은 문법 설명용이다. 완성 코드에서는 목록 지향 출력 대신 서식을 지정한다. type is 분기 안에서는 선택한 이름을 해당 타입으로 다룰 수 있으므로 자식의 성분에 접근할 수 있다. 이 범위를 벗어나면 다시 원래 선언의 제약을 따른다.
type is는 동적 타입이 지정한 타입과 정확히 같을 때 선택된다. class is는 지정한 타입과 그 확장 타입을 함께 다룬다. 두 문장을 섞어 쓰더라도 단순히 위에서부터 처음 맞는 분기를 선택한다고 해석하면 안 된다. 정확히 일치하는 type is가 우선하며, 적용 가능한 class is가 여러 개면 더 구체적인 타입에 해당하는 분기가 선택된다. class default는 어떤 타입 가드에도 해당하지 않을 때의 처리다.
예제의 describe_boundary는 구체적인 두 타입의 설정만 출력한다. 새로운 경계 타입을 추가해도 공통 apply 호출은 작동하지만, 설명 출력은 기본 분기로 들어간다. 따라서 타입 분기를 사용하는 곳은 새 타입을 추가할 때 함께 검토할 지점이다.
class(*)는 공통 부모까지 없앤다
class(*)는 선언에서 특정 부모 타입을 지정하지 않는다. 정수, 실수, 문자, 파생 타입 등 서로 다른 타입의 값을 받을 수 있다. 다만 타입과 종류를 열어 두는 선언이지 차원 수까지 자동으로 바꾸는 선언은 아니다. 예제의 class(*) :: value는 스칼라 더미 인수이므로 스칼라 값을 전달한다.
무제한 다형성 값은 분기 밖에서 바로 덧셈하거나 특정 성분을 읽을 수 없다. 어떤 타입인지 정해지지 않았기 때문이다. select type으로 타입을 확인한 범위에서 해당 타입에 허용되는 연산과 출력 서식을 사용한다. 완성 코드의 print_value는 문자, 기본 정수, real64 실수, 기본 논리를 구별해서 출력한다.
type is (real(real64))는 모든 실수 종류를 받는 분기가 아니다. 해당 종류의 실수만 일치한다. 다른 종류의 실수까지 지원하려면 그 종류에 대한 분기를 따로 마련하거나, 호출 전에 정한 종류로 변환한다. 문자열은 type is (character(len=*))로 길이가 서로 다른 기본 문자 값을 다룬다.
경계 조건의 실행 인수를 class(*)로 바꾸면 타입을 넓게 받기는 쉽다. 그러나 부모의 apply라는 계약도 함께 사라져 분기마다 동작을 직접 골라야 한다. 공통 동작이 있는 객체에는 class(boundary_t)를 사용하고, 여러 타입의 단순 값을 표시하는 보조 루틴에는 class(*)를 사용하는 것이 이번 예제의 선택이다.
| 표현 | 허용하거나 선택하는 대상 | 예제의 용도 |
|---|---|---|
type(fixed_boundary_t) | 고정 온도 타입의 비다형성 객체 | 종류가 정해진 지역 객체 |
class(boundary_t) | 경계 타입과 그 확장 타입 | 공통 바인딩 호출 |
class(*) | 특정 부모로 제한하지 않은 타입 | 스칼라 진단 값 출력 |
type is / class is | 정확한 타입 / 타입과 확장 타입 | 분기 안에서 타입별 접근 |
완성 코드
모듈과 주 프로그램을 한 파일에 둔다. 고정 온도 조건은 네 가장자리를 모두 지정한 온도로 덮어쓴다. 단열 조건은 가장자리 온도를 바로 안쪽 온도와 같게 만든다. 이는 경계에 수직인 방향의 온도 차이를 0으로 두는 간단한 표현이다. 모서리는 가로 가장자리를 처리한 다음 세로 가장자리를 처리하는 순서에 따라 정해진다.
단열 조건의 enabled는 적용을 잠시 끌 수 있는 설정이다. 예제에서는 참으로 두어 이웃 값 복사가 실행되게 한다. 각 구현은 가로와 세로가 세 칸 이상인지 확인한다. 내부 칸이 없는 배열에는 이 예제의 경계 규칙을 적용하지 않는다.
main.f90
module boundary_model
use iso_fortran_env, only: real64
implicit none
private
public :: boundary_t, fixed_boundary_t, insulated_boundary_t
public :: describe_boundary, print_value
type, abstract :: boundary_t
contains
procedure(apply_interface), deferred :: apply
end type boundary_t
type, extends(boundary_t) :: fixed_boundary_t
real(real64) :: temperature = 10.0_real64
contains
procedure :: apply => apply_fixed
end type fixed_boundary_t
type, extends(boundary_t) :: insulated_boundary_t
logical :: enabled = .true.
contains
procedure :: apply => apply_insulated
end type insulated_boundary_t
abstract interface
subroutine apply_interface(self, temperature)
import :: boundary_t, real64
class(boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
end subroutine apply_interface
end interface
contains
subroutine apply_fixed(self, temperature)
class(fixed_boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
integer :: nx, ny
nx = size(temperature, 1)
ny = size(temperature, 2)
if (nx < 3 .or. ny < 3) error stop 'grid too small'
temperature(1, :) = self%temperature
temperature(nx, :) = self%temperature
temperature(:, 1) = self%temperature
temperature(:, ny) = self%temperature
end subroutine apply_fixed
subroutine apply_insulated(self, temperature)
class(insulated_boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
integer :: nx, ny
nx = size(temperature, 1)
ny = size(temperature, 2)
if (nx < 3 .or. ny < 3) error stop 'grid too small'
if (.not. self%enabled) return
temperature(1, 2:ny-1) = temperature(2, 2:ny-1)
temperature(nx, 2:ny-1) = temperature(nx-1, 2:ny-1)
temperature(:, 1) = temperature(:, 2)
temperature(:, ny) = temperature(:, ny-1)
end subroutine apply_insulated
subroutine describe_boundary(condition)
class(boundary_t), intent(in) :: condition
select type (condition)
type is (fixed_boundary_t)
call print_value('fixed temperature', condition%temperature)
type is (insulated_boundary_t)
call print_value('insulation enabled', condition%enabled)
class default
write(*, '(A)') 'boundary details unavailable'
end select
end subroutine describe_boundary
subroutine print_value(label, value)
character(len=*), intent(in) :: label
class(*), intent(in) :: value
select type (value)
type is (integer)
write(*, '(A, ": ", I0)') trim(label), value
type is (real(real64))
write(*, '(A, ": ", F8.2)') trim(label), value
type is (character(len=*))
write(*, '(A, ": ", A)') trim(label), trim(value)
type is (logical)
write(*, '(A, ": ", L1)') trim(label), value
class default
write(*, '(A, ": unsupported type")') trim(label)
end select
end subroutine print_value
end module boundary_model
program main
use iso_fortran_env, only: real64
use boundary_model, only: boundary_t, fixed_boundary_t, &
insulated_boundary_t, &
describe_boundary, print_value
implicit none
class(boundary_t), allocatable :: condition
allocate(fixed_boundary_t :: condition)
call run_case(condition, 'fixed')
deallocate(condition)
allocate(insulated_boundary_t :: condition)
call run_case(condition, 'insulated')
deallocate(condition)
contains
subroutine run_case(condition, label)
class(boundary_t), intent(in) :: condition
character(len=*), intent(in) :: label
real(real64) :: temperature(5, 4)
temperature = 20.0_real64
temperature(3, 2) = 80.0_real64
temperature(3, 3) = 60.0_real64
call print_value('case', label)
call describe_boundary(condition)
call condition%apply(temperature)
write(*, '(A, 5F8.2)') 'row j=2:', temperature(:, 2)
call print_value('cell count', size(temperature))
call print_value('temperature sum', sum(temperature))
end subroutine run_case
end program main
줄별 해설
use iso_fortran_env, only: real64는 온도 배열과 출력 루틴이 같은 실수 종류를 사용하게 한다. 모듈의 기본 공개 범위는 private로 두고, 주 프로그램에 필요한 타입과 보조 루틴만 공개한다. 실제 적용 프로시저의 이름은 공개하지 않아도 공개된 타입의 바인딩을 통해 호출할 수 있다.
type, abstract :: boundary_t부터 부모 타입을 정의한다. 부모는 온도 배열을 보관하지 않는다. 경계 조건 객체의 책임은 전달받은 배열의 가장자리를 설정하는 것이며, 배열의 소유와 초기화는 호출하는 쪽에 남긴다. 이렇게 하면 여러 크기의 배열에 같은 조건 객체를 사용할 수 있다.
procedure(apply_interface), deferred :: apply는 이름과 형식을 연결한다. 이어지는 두 자식 타입은 각각 하나의 설정 성분을 추가하고 apply를 재정의한다. 부모에서 약속한 호출 형식이 같기 때문에 run_case는 어느 자식을 받았는지 검사하지 않고 적용을 요청할 수 있다.
abstract interface 블록에는 실행할 계산문이 없다. import, 더미 인수 선언, 종료문으로 호출 형식만 기술한다. 이 블록은 부모 타입 선언 뒤에 두어 부모 이름을 가져오게 한다. 실제 계산은 모듈의 contains 뒤에 있는 프로시저에서 수행한다.
apply_fixed의 두 size 호출은 각 방향의 칸 수를 구한다. 네 배열 대입은 왼쪽, 오른쪽, 아래쪽, 위쪽 가장자리를 차례로 설정한다. 모서리는 두 번 대입되지만 같은 온도를 쓰므로 결과는 같다. 내부의 80과 60은 이 대입 범위에 포함되지 않는다.
apply_insulated는 먼저 모서리를 제외한 왼쪽과 오른쪽 가장자리를 복사한다. 다음 두 문장이 아래쪽과 위쪽 행 전체를 복사하면서 모서리도 채운다. 모서리를 포함한 전체 열을 먼저 복사하면 아직 처리되지 않은 위아래 경계 값을 읽을 수 있으므로, 이 예제는 처리 범위를 명시적으로 나눈다.
describe_boundary의 타입 분기는 설정을 표시하는 데만 사용한다. condition%temperature는 고정 온도 타입 분기 안에서, condition%enabled는 단열 타입 분기 안에서만 유효하다. 반면 실제 경계 적용은 이 루틴에서 실행하지 않는다.
print_value는 레이블을 기본 문자로 고정하고 값만 무제한 다형성으로 받는다. 정수에는 I0, 실수에는 F8.2, 논리에는 L1을 쓴다. 실수 출력의 폭과 소수 자릿수가 정해져 있으므로 실행 결과를 비교하기 쉽다. 지원하지 않는 타입은 기본 분기에서 정해진 문장을 출력한다.
주 프로그램의 첫 allocate는 고정 온도 객체를 만든다. run_case가 끝나면 객체를 해제하고 단열 객체를 새로 만든다. run_case의 더미 인수에는 allocatable 속성이 없다. 객체를 읽고 바인딩을 호출할 뿐, 할당 상태를 바꾸지 않기 때문이다.
run_case가 호출될 때마다 온도 배열을 다시 초기화한다. 단열 조건이 고정 온도 조건의 결과를 이어받지 않도록 하는 부분이다. 마지막 두 호출은 스칼라 기본 정수와 real64 실수를 같은 출력 루틴으로 전달한다. 한 프로그램에서 상속 관계를 사용하는 다형성과 상속 관계를 요구하지 않는 값 전달을 함께 확인할 수 있다.
실행 결과
다음 명령으로 컴파일하고 실행한다. 한 파일 안에서 모듈이 주 프로그램보다 먼저 정의되어 있으므로 별도의 소스 파일 순서를 지정할 필요가 없다. 정상 실행은 입력을 요청하지 않는다.
gfortran -std=f2018 -Wall main.f90 -o heat_boundary
./heat_boundary
예상 출력은 다음과 같다. 실수 앞의 공백은 F8.2의 출력 폭에 따른 것이다.
case: fixed
fixed temperature: 10.00
row j=2: 10.00 20.00 80.00 20.00 10.00
cell count: 20
temperature sum: 360.00
case: insulated
insulation enabled: T
row j=2: 20.00 20.00 80.00 20.00 20.00
cell count: 20
temperature sum: 600.00
고정 온도 조건에서는 가장자리 열네 칸의 합이 140이다. 내부 여섯 칸은 20 네 개와 80, 60이므로 합이 220이고, 전체 합은 360이다. 단열 조건에서는 아래 행의 가운데에 80이, 위 행의 가운데에 60이 복사된다. 기본 합 400에 80인 두 칸의 증가량 120과 60인 두 칸의 증가량 80을 더하면 600이 된다.
여기서 온도 합은 배열 처리 결과를 확인하는 값이다. 경계 값을 덮어쓰는 이 예제에서 합이 보존될 이유는 없으며, 단순한 온도 합을 금속판의 총 열량으로 해석해서도 안 된다. 실제 열량에는 칸의 면적, 두께, 밀도, 비열과 경계의 수치적 의미가 함께 필요하다.
실무에서 자주 틀리는 것
추상 타입 자체를 객체로 선언한다
추상 타입은 실제 구현을 선택하기 위한 기준이다. 다음 선언은 직접 객체를 만들려고 하므로 허용되지 않는다.
type(boundary_t) :: condition
구체 타입을 선언하거나, 다형성 할당 변수에 구체 타입으로 객체를 만든다.
class(boundary_t), allocatable :: condition
allocate(fixed_boundary_t :: condition)
할당 변수는 선언만으로 적용 가능한 객체가 되지 않는다. 바인딩을 호출하기 전에 할당되어 있어야 한다. 추상 부모를 그대로 할당하는 대신 구현이 갖춰진 자식 타입을 지정한다.
전달 객체 인수를 type으로 선언한다
다음은 확장 가능한 타입의 형식 바인딩에 연결할 구현으로는 잘못된 선언이다.
subroutine apply_fixed(self, temperature)
type(fixed_boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
end subroutine apply_fixed
전달 객체 인수를 다형성으로 선언한다. 인터페이스와 구현의 나머지 인수 특성도 함께 맞춘다.
subroutine apply_fixed(self, temperature)
class(fixed_boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
temperature(1, :) = self%temperature
end subroutine apply_fixed
고친 조각은 전달 객체 선언을 보여주는 최소 예다. 실제 경계 전체를 설정하는 구현은 완성 코드처럼 네 가장자리를 처리해야 한다.
부모 선언에서 자식 성분을 바로 읽는다
동적 타입이 고정 온도 타입일 것이라고 예상해도, 부모 선언만으로 자식 성분에 접근할 수는 없다.
class(boundary_t), intent(in) :: condition
write(*, '(F8.2)') condition%temperature
타입을 구별한 범위 안에서 읽거나, 모든 경계 타입에 필요한 정보라면 부모의 공통 바인딩으로 설계한다. 이번 예제의 설정 출력은 다음처럼 처리한다.
select type (condition)
type is (fixed_boundary_t)
write(*, '(F8.2)') condition%temperature
class default
write(*, '(A)') 'no fixed temperature'
end select
반대로 경계 적용을 위해 이 분기를 반복해서 만들 필요는 없다. 이미 공통 바인딩으로 선언한 동작은 call condition%apply(temperature)로 요청한다.
class(*)를 바로 계산하거나 출력한다
무제한 다형성으로 받았다는 이유만으로 값이 실수라고 가정할 수 없다. 다음 조각은 허용되지 않는다.
class(*), intent(in) :: value
write(*, '(F8.2)') value + 1.0_real64
실수 종류까지 확인한 뒤 계산한다. 다른 타입이 들어올 경우도 명시한다.
select type (value)
type is (real(real64))
write(*, '(F8.2)') value + 1.0_real64
class default
write(*, '(A)') 'real64 value required'
end select
모든 호출이 실수만 전달한다면 처음부터 real(real64) 더미 인수를 사용하는 편이 간단하다. class(*)는 서로 다른 타입을 받아야 하는 요구가 있을 때 선택한다.
한눈에 보기
| 문법 | 핵심 규칙 | 설계에서의 역할 |
|---|---|---|
abstract | 해당 타입 자체의 객체를 만들 수 없다 | 공통 개념을 구체 구현과 분리한다 |
deferred | 구현 대신 명시적 인터페이스를 연결한다 | 구체 자식이 제공할 동작을 정한다 |
abstract interface | 프로시저 형식만 선언한다 | 바인딩 구현의 인수 특성을 맞춘다 |
class(parent) | 부모와 확장 타입을 받는다 | 공통 호출로 다른 구현을 실행한다 |
select type | 분기 범위에서 타입이 구체화된다 | 타입별 성분과 연산에 접근한다 |
class(*) | 특정 부모로 제한하지 않는다 | 다양한 타입의 값을 보조 루틴에 전달한다 |
설계의 중심은 타입 검사 횟수를 늘리는 데 있지 않다. 모든 경계 조건에 필요한 동작은 부모의 바인딩으로 선언한다. 구체 타입에만 필요한 설정을 살펴볼 때 타입 분기를 사용한다. 공통 부모를 두기 어려운 단순 값 전달에는 무제한 다형성을 사용할 수 있다.
다음 장에서는 호출 이름을 정리하는 다른 방법을 다룬다. 여기서 만든 추상 계약과 동적 타입에 따른 호출을 이해해 두면, 여러 인수 형식에 같은 이름을 붙이는 기능과의 차이를 구별할 수 있다.
연습 문제
- 첫 번째 경계 객체를 할당한 뒤 고정 온도를 5로 바꾸어라. 부모 타입으로 선언된
condition을 유지하고, 타입 분기를 사용해 설정을 변경하라. 두 번째 행과 전체 온도 합을 예측하라. - 단열 객체의
enabled를 거짓으로 바꾸어라. 출력되는 두 번째 행과 온도 합을 예측하고, 고정 온도 조건의 결과가 이어지지 않는 이유를 설명하라. print_value에real32실수를 처리하는 분기를 추가하라. 모듈에서 필요한 이름을 가져오고, 값2.5_real32를 출력하라. 기존real64분기가 이 값을 처리하지 않는 이유를 설명하라.zero_boundary_t라는 새 구체 타입을 만들어 네 가장자리를 0으로 설정하라. 전달 객체 인수를class(zero_boundary_t)로 선언하고, 설정 성분을 사용하도록 구현하라.run_case를 바꾸어야 하는지와 예상 온도 합을 설명하라.
정답과 해설
1. 고정 온도 변경
첫 번째 allocate와 run_case 호출 사이에 다음 문장을 넣는다. 타입 가드 안에서는 고정 온도 타입의 성분을 변경할 수 있다.
select type (condition)
type is (fixed_boundary_t)
condition%temperature = 5.0_real64
end select
두 번째 행은 5, 20, 80, 20, 5다. 가장자리 열네 칸의 합은 70이고 내부 합은 220이므로 전체 합은 290이다. 설정 출력도 5.00으로 바뀐다. 적용 루틴은 객체의 설정을 읽기만 하므로 수정할 필요가 없다.
2. 단열 적용 끄기
두 번째 객체를 할당한 뒤 다음 분기를 추가한다.
select type (condition)
type is (insulated_boundary_t)
condition%enabled = .false.
end select
설정 출력은 F가 된다. 적용 루틴이 배열 크기를 확인한 다음 돌아오므로 두 번째 행은 20, 20, 80, 20, 20이고 전체 합은 500이다. 두 번째 행만 보면 적용이 켜진 경우와 같지만 전체 합은 다르다. run_case에서 매번 배열 전체를 초기화하므로 첫 번째 실행의 고정 온도 값은 남지 않는다.
3. 다른 실수 종류 지원
모듈의 가져오기 목록에 real32를 추가하고, print_value의 선택 구문에 다음 분기를 넣는다. 기존 real64 분기는 유지한다.
type is (real(real32))
write(*, '(A, ": ", F8.2)') trim(label), value
주 프로그램에서도 real32를 가져온 뒤 다음 호출로 확인한다.
call print_value('small value', 2.5_real32)
출력은 다음과 같다. 타입 가드는 실수라는 분류뿐 아니라 종류도 일치해야 하므로 real64 분기는 이 값을 선택하지 않는다.
small value: 2.50
4. 새 경계 타입 추가
모듈의 타입 선언 부분에 다음 구체 타입을 추가하고 공개 목록에 넣는다. 기본 설정을 0으로 둔다.
type, extends(boundary_t) :: zero_boundary_t
real(real64) :: edge_value = 0.0_real64
contains
procedure :: apply => apply_zero
end type zero_boundary_t
모듈의 contains 뒤에 구현을 추가한다. 전달 객체의 설정을 읽으므로 객체 인수도 실제 동작에 참여한다.
subroutine apply_zero(self, temperature)
class(zero_boundary_t), intent(in) :: self
real(real64), intent(inout) :: temperature(:, :)
integer :: nx, ny
nx = size(temperature, 1)
ny = size(temperature, 2)
if (nx < 3 .or. ny < 3) error stop 'grid too small'
temperature(1, :) = self%edge_value
temperature(nx, :) = self%edge_value
temperature(:, 1) = self%edge_value
temperature(:, ny) = self%edge_value
end subroutine apply_zero
주 프로그램で新しい型名を取り込む必要がある。
주 프로그램에서 새 타입 이름을 가져온 뒤 기존 객체를 해제한 상태에서 allocate(zero_boundary_t :: condition)과 call run_case(condition, 'zero')를 실행한다. run_case는 공통 바인딩을 호출하므로 수정하지 않는다. 가장자리 합이 0이고 내부 합이 220이므로 전체 합은 220이다.
describe_boundary에는 새 타입 분기가 없으므로 기본 안내 문장이 출력된다. 구체적인 설정을 보여주려면 그 루틴에 분기를 추가한다. 다만 이번 조건은 고정 온도 타입의 설정을 0으로 바꾸어도 표현할 수 있다. 별도 타입을 만드는 문제는 확장 절차를 확인하기 위한 것이며, 실제 설계에서는 설정 값의 차이만으로 타입을 늘릴 필요가 있는지 먼저 판단한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.