Devin.KR

제네릭 인터페이스와 연산자 정의

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서 추상 타입과 다형성으로 계산 대상의 공통 규약을 표현했다. 이번에는 호출하는 쪽의 표기를 다듬는다. 같은 계산 의도를 가진 프로시저를 하나의 이름으로 묶고, 직접 만든 타입에도 덧셈과 곱셈 같은 표기를 부여한다. 코드가 짧아지는 것보다 중요한 목표는 계산식에서 자료의 의미가 드러나게 하는 것이다.

2차원 금속판의 온도는 스칼라지만, 온도가 증가하는 방향과 열이 흐르는 방향은 두 성분으로 표현한다. 이 장에서는 이 두 성분을 담는 벡터 타입을 만들고, 온도 기울기와 열 흐름을 계산하는 작은 프로그램을 완성한다. 격자 전체의 시간을 진행시키는 계산은 다루지 않는다. 판의 한 지점 주변에서 얻은 온도로 벡터 연산의 의미를 확인한다.

  • 제네릭 인터페이스(generic interface)로 여러 프로시저에 하나의 호출 이름을 제공한다.
  • 사용자 정의 연산자(user-defined operator)인 .dot.으로 두 벡터의 내적을 표현한다.
  • 연산자 중복 정의(operator overloading)로 벡터 덧셈과 스칼라 곱셈을 구현한다.
  • 정의 대입(defined assignment)으로 실수 배열의 두 값을 벡터에 저장한다.
  • 호출을 구별하는 규칙과 연산자의 우선순위를 이용해 모호하지 않은 수식을 작성한다.

문제 상황

금속판의 한 지점에서 왼쪽과 오른쪽의 온도 차이를 구하면 가로 방향 변화량을 얻는다. 아래쪽과 위쪽에서도 같은 계산을 하면 세로 방향 변화량을 얻는다. 두 값을 별개 변수로 관리하면 계산식마다 성분 이름이 반복된다. 열전도율을 곱하거나 두 방향의 관계를 확인할 때도 가로와 세로에 같은 작업을 두 번 적게 된다.

flux_x = -conductivity * gradient_x
flux_y = -conductivity * gradient_y
alignment = flux_x * gradient_x + flux_y * gradient_y

이 코드는 계산 자체는 분명하지만, 함께 움직이는 두 성분이라는 관계를 이름의 접미사에 의존한다. 변수를 잘못 섞어도 모두 같은 실수 종류이면 컴파일러가 오류를 찾아주기 어렵다. 두 성분을 하나의 타입으로 묶으면 프로시저의 인자와 결과에 벡터라는 단위를 표시할 수 있다.

다만 타입만 만든다고 수식이 읽기 쉬워지는 것은 아니다. 모든 계산을 add_vectors, scale_vector, dot_vectors처럼 호출하면 간단한 식도 여러 함수 호출로 둘러싸인다. 이번 장의 목표는 이런 구현 프로시저를 모듈 안에 두고, 사용하는 쪽에는 a + b, s * a, a .dot. b라는 일관된 표기를 제공하는 것이다.

예제에서 사용하는 열 흐름 벡터는 온도 기울기에 음의 열전도율을 곱한 값이다. 두 벡터의 내적이 음수이면 열 흐름이 온도가 증가하는 방향과 반대라는 것을 확인할 수 있다. 물리적 해석이 붙어 있으므로 연산자가 실제로 어떤 계산을 해야 하는지도 분명해진다.

같은 이름으로 서로 다른 프로시저 호출하기

제네릭 이름은 여러 구현 프로시저에 붙이는 공통 이름이다. 컴파일러는 호출에 사용한 인자의 타입, 종류, 차원 수와 인자 구성 등을 보고 해당 구현을 선택한다. 이때 각 구현을 구체 프로시저(specific procedure)라고 부른다. 제네릭 자체가 실행할 계산을 갖는 것은 아니다.

예제에서는 make_vector를 공통 이름으로 사용한다. 실수 두 개를 전달하면 실수를 그대로 저장하는 함수가 선택되고, 정수 두 개를 전달하면 정수를 예제의 실수 종류로 변환하는 함수가 선택된다. 사용하는 쪽은 변환 여부를 구현 이름으로 표현하지 않아도 된다.

interface make_vector
  module procedure vector_from_real
  module procedure vector_from_integer
end interface

module procedure 뒤의 이름은 같은 모듈에서 정의하는 프로시저 이름이다. 이 인터페이스 블록은 모듈의 contains 앞에 둔다. 함수 본체는 contains 뒤에 둔다. 외부에 공개할 것은 make_vector이고, 두 구현 이름은 비공개로 유지할 수 있다.

호출 인자에 따라 선택되는 벡터 생성 함수
호출인자 구성선택되는 구현
make_vector(1.0_wp, 0.0_wp)예제 실수 종류 두 개vector_from_real
make_vector(1, 0)기본 정수 두 개vector_from_integer
make_vector(1.0_wp, 0)실수와 정수의 혼합일치하는 구현 없음
제네릭 이름은 호출 인자의 타입과 종류를 보고 구체 프로시저를 선택한다

제네릭 선택은 자동 변환을 시도하는 과정이 아니다. 실수 인자를 받는 함수가 있다고 해서 정수 인자가 그 함수에 맞게 바뀌지 않는다. 위 표의 정수 호출이 가능한 이유는 정수용 함수를 별도로 등록했기 때문이다. 마찬가지로 1.0과 1.0_wp는 실수라는 큰 분류가 같아도 종류가 다를 수 있다. 예제의 실수용 함수는 real(wp)를 받으므로 호출에서도 그 종류를 맞춘다.

함수의 결과 타입만 다르게 만들어서는 제네릭 호출을 구별할 수 없다. 왼쪽 변수의 타입을 보고 오른쪽 함수의 구현을 고르는 방식으로 설계하면 안 된다. 호출에 들어가는 인자가 구별 근거를 제공해야 한다.

차원 수로 구별할 때도 배열의 원소 개수와 배열의 차원 수를 혼동하지 않아야 한다. 길이가 2인 일차원 배열과 길이가 3인 일차원 배열은 모두 차원 수가 1이다. 원소 개수만 다른 배열 인자를 각각 받는 두 함수를 같은 제네릭에 넣는다고 호출이 구별되지는 않는다. 필요한 경우 하나의 배열용 함수가 크기를 확인하도록 설계한다.

앞 장의 다형성과도 구별할 필요가 있다. 이 예제의 제네릭은 호출 인자의 선언된 특성으로 사용할 함수를 결정한다. 실행 중 객체의 동적 타입에 따라 재정의된 바인딩을 선택하는 기능과는 역할이 다르다. 두 기능을 함께 사용할 수 있지만, 먼저 이 장처럼 단순한 구체 타입으로 선택 규칙을 확인하는 편이 이해하기 쉽다.

벡터 연산을 수식으로 표현하기

덧셈과 스칼라 곱셈

벡터 타입은 x와 y라는 두 실수 성분을 갖는다. 덧셈 함수는 각 성분끼리 더한 새 벡터를 반환한다. 스칼라 곱셈 함수는 두 성분에 같은 실수를 곱한다. 원래 인자는 변경하지 않는다.

interface operator(+)
  module procedure add_vectors
end interface

interface operator(*)
  module procedure scale_left
  module procedure scale_right
end interface

operator(+)는 새 기호를 만드는 선언이 아니다. 이미 있는 덧셈 기호에 벡터 두 개를 받는 계산을 추가한다. 실수끼리의 덧셈은 기존 의미를 유지한다. 벡터끼리의 덧셈에서만 등록한 함수가 사용된다.

곱셈에는 두 함수를 등록한다. scale_left는 실수와 벡터를 이 순서로 받고, scale_right는 벡터와 실수를 받는다. 한쪽 순서를 등록했다고 다른 쪽도 자동으로 지원되지는 않는다. 두 표기를 제공하려면 각각 일치하는 인자 구성이 필요하다. 오른쪽 곱셈 함수는 왼쪽 곱셈 함수를 호출해 실제 성분 계산을 한곳에 모은다.

기존 기호를 중복 정의해도 그 기호의 우선순위는 바뀌지 않는다. a + s * b에서는 곱셈이 먼저 수행된다. 또한 이번에 정의한 덧셈은 이항 덧셈이다. 인자 두 개를 받는 덧셈 함수를 등록하는 것만으로 단항 부호 연산인 -a까지 지원되지는 않는다.

내적을 나타내는 .dot.

벡터 두 개의 내적은 대응하는 성분을 곱한 뒤 더한 실수다. 이 계산은 결과가 벡터인 덧셈과 다르다. 예제에서는 의미가 드러나는 이름을 점으로 둘러싸서 .dot.라는 연산자로 제공한다.

interface operator(.dot.)
  module procedure dot_vectors
end interface

이항 연산자를 구현하는 함수는 두 피연산자를 더미 인자 두 개로 받는다. 이 장의 연산자 함수에서는 두 인자 모두 intent(in)으로 선언한다. 선택 인자는 사용하지 않는다. 연산 결과는 함수 결과로 돌려준다. 연산자가 입력 객체를 바꾸지 않는다는 규칙을 지키면 복합 수식의 의미를 추적하기 쉽다.

사용자 정의 이항 연산자는 덧셈이나 곱셈 같은 내장 연산자보다 우선순위가 낮다. 따라서 내적 결과에 값을 더할 때는 center + (gradient .dot. offset)처럼 내적을 괄호로 묶는다. 괄호 없이 center + gradient .dot. offset을 쓰면 먼저 center + gradient를 묶어 해석하므로, 이번 타입이 제공하지 않는 실수와 벡터의 덧셈이 요구된다.

괄호는 오류를 피하는 장치이면서 계산 의도를 설명하는 표기다. 특히 내적의 결과가 실수라는 점을 괄호로 드러내면, 벡터 계산과 스칼라 계산이 만나는 지점을 읽기 쉬워진다.

벡터 덧셈과 스칼라 곱셈은 벡터를 반환하고 내적은 실수를 반환한다

대입을 정의할 때 지킬 경계

정의 대입은 대입문의 왼쪽과 오른쪽 타입 조합에 맞는 서브루틴을 연결한다. 연산자 구현이 함수인 것과 달리, 대입 구현은 왼쪽 객체를 직접 설정하는 서브루틴이다. 예제에서는 실수 일차원 배열을 오른쪽에 놓고 벡터를 왼쪽에 놓을 수 있게 한다.

interface assignment(=)
  module procedure assign_from_array
end interface

구현의 첫 번째 인자는 대입문의 왼쪽이며 intent(out)으로 선언한다. 두 번째 인자는 오른쪽이며 intent(in)으로 선언한다. 오른쪽 배열은 가정 형상 배열로 받아 크기를 검사한다. 원소가 두 개가 아니면 정상적인 벡터를 만들 수 없으므로 프로그램을 오류 종료한다.

이 기능은 자료 표현을 바꾸는 경계에서 유용하다. 예를 들어 두 좌표를 배열로 받은 다음 벡터 계산에 넘길 때 offset = [0.25_wp, -0.50_wp]로 표현할 수 있다. 그러나 모든 타입 사이의 변환을 대입에 숨길 필요는 없다. 단위 변환이나 손실이 큰 변환이라면 이름 있는 함수가 계산 의도를 더 잘 드러낼 수 있다.

벡터를 다른 벡터에 복사하는 대입은 이 모듈에서 별도로 정의하지 않는다. a = b처럼 양쪽이 같은 벡터 타입이면 기본 대입이 두 성분을 복사한다. 실수 배열을 받는 정의 대입을 추가했다고 모든 대입이 그 서브루틴을 거치는 것은 아니다.

인터페이스의 공개 범위도 점검해야 한다. 모듈 전체를 비공개로 시작했다면 타입과 생성 함수뿐 아니라 연산자와 대입 인터페이스도 공개해야 사용자가 쓸 수 있다. 구현 함수 이름은 계속 숨길 수 있다. 사용자는 계산의 표기를 알고, 모듈은 그 표기의 구현을 관리한다.

완성 코드

다음 프로그램은 판의 중심 온도를 60으로 두고, 중심에서 같은 거리만큼 떨어진 네 지점의 온도를 사용한다. 가로와 세로 간격은 각각 0.5다. 오른쪽 온도에서 왼쪽 온도를 뺀 값을 두 지점 사이의 거리로 나누고, 위쪽과 아래쪽에도 같은 계산을 적용한다. 열전도율은 2로 둔다.

이 프로그램은 공간 벡터를 표현하는 예제다. 온도장 전체를 갱신하거나 경계 조건을 적용하지 않는다. 마지막 온도 추정값은 중심의 기울기가 작은 변위 구간에서도 일정하다고 가정한 일차 근사다.

main.f90

module plate_vectors
  use iso_fortran_env, only: real64
  implicit none
  private

  integer, parameter, public :: wp = real64

  type, public :: vector2
    real(wp) :: x = 0.0_wp
    real(wp) :: y = 0.0_wp
  end type vector2

  public :: make_vector
  public :: operator(+), operator(*), operator(.dot.)
  public :: assignment(=)

  interface make_vector
    module procedure vector_from_real
    module procedure vector_from_integer
  end interface

  interface operator(+)
    module procedure add_vectors
  end interface

  interface operator(*)
    module procedure scale_left
    module procedure scale_right
  end interface

  interface operator(.dot.)
    module procedure dot_vectors
  end interface

  interface assignment(=)
    module procedure assign_from_array
  end interface

contains

  pure function vector_from_real(x, y) result(v)
    real(wp), intent(in) :: x, y
    type(vector2) :: v

    v%x = x
    v%y = y
  end function vector_from_real

  pure function vector_from_integer(x, y) result(v)
    integer, intent(in) :: x, y
    type(vector2) :: v

    v = vector_from_real(real(x, wp), real(y, wp))
  end function vector_from_integer

  pure function add_vectors(a, b) result(v)
    type(vector2), intent(in) :: a, b
    type(vector2) :: v

    v%x = a%x + b%x
    v%y = a%y + b%y
  end function add_vectors

  pure function scale_left(s, a) result(v)
    real(wp), intent(in) :: s
    type(vector2), intent(in) :: a
    type(vector2) :: v

    v%x = s * a%x
    v%y = s * a%y
  end function scale_left

  pure function scale_right(a, s) result(v)
    type(vector2), intent(in) :: a
    real(wp), intent(in) :: s
    type(vector2) :: v

    v = scale_left(s, a)
  end function scale_right

  pure function dot_vectors(a, b) result(value)
    type(vector2), intent(in) :: a, b
    real(wp) :: value

    value = a%x * b%x + a%y * b%y
  end function dot_vectors

  subroutine assign_from_array(lhs, rhs)
    type(vector2), intent(out) :: lhs
    real(wp), intent(in) :: rhs(:)

    if (size(rhs) /= 2) then
      error stop 'vector assignment requires two elements'
    end if

    lhs%x = rhs(1)
    lhs%y = rhs(2)
  end subroutine assign_from_array

end module plate_vectors

program main
  use plate_vectors, only: wp, vector2, make_vector, &
                          operator(+), operator(*), &
                          operator(.dot.), assignment(=)
  implicit none

  real(wp), parameter :: dx = 0.5_wp, dy = 0.5_wp
  real(wp), parameter :: conductivity = 2.0_wp
  real(wp), parameter :: center = 60.0_wp
  real(wp), parameter :: left = 80.0_wp, right = 40.0_wp
  real(wp), parameter :: lower = 50.0_wp, upper = 70.0_wp

  type(vector2) :: gradient, flux, offset, combined, axis
  real(wp) :: alignment, estimate, projection

  gradient = make_vector((right - left) / (2.0_wp * dx), &
                         (upper - lower) / (2.0_wp * dy))
  flux = (-conductivity) * gradient
  offset = [0.25_wp, -0.50_wp]
  combined = flux + flux * 0.5_wp
  axis = make_vector(1, 0)

  alignment = flux .dot. gradient
  estimate = center + (gradient .dot. offset)
  projection = flux .dot. axis

  write(*, '(a,2f8.2)') 'gradient:', gradient%x, gradient%y
  write(*, '(a,2f8.2)') 'flux:    ', flux%x, flux%y
  write(*, '(a,2f8.2)') 'offset:  ', offset%x, offset%y
  write(*, '(a,2f8.2)') 'combined:', combined%x, combined%y
  write(*, '(a,f10.2)') 'flux dot gradient:', alignment
  write(*, '(a,f8.2)') 'estimated temperature:', estimate
  write(*, '(a,f8.2)') 'x projection:', projection
end program main

줄별 해설

use iso_fortran_env, only: real64와 wp 선언은 모듈과 주 프로그램이 같은 실수 종류를 사용하게 한다. 정수용 생성 함수는 real(x, wp)처럼 변환 결과의 종류를 명시한다. 정수를 실수로 바꾸는 책임이 생성 함수 안에 모여 있다.

private는 이름을 기본적으로 숨긴다. 이어지는 공개 선언은 사용자가 알아야 할 타입, 실수 종류, 생성 함수, 연산자와 대입만 드러낸다. 이 예제의 성분 x와 y는 공개 상태이므로 출력문에서 직접 읽을 수 있다. 성분을 숨기고 접근 함수를 제공하는 설계도 가능하지만 여기서는 인터페이스의 선택 규칙에 집중한다.

타입 선언의 두 초기값은 기본 초기화다. 단순히 type(vector2) :: a라고 선언한 객체도 두 성분의 초기값을 갖는다. 생성 함수와 연산자 함수에서는 결과의 두 성분을 모두 설정한다. 따라서 결과가 초기값에 우연히 의존하지 않는다.

다섯 인터페이스 블록은 공개 표기와 구현의 연결표다. make_vector와 곱셈에는 구현이 두 개씩 연결된다. 덧셈과 내적, 배열 대입에는 각각 하나가 연결된다. 같은 구문 형태의 인터페이스라도 연산자는 함수에, 대입은 서브루틴에 연결된다는 차이가 있다.

vector_from_real은 두 값을 결과 성분에 넣는다. vector_from_integer는 두 값을 변환한 후 실수용 구현을 직접 호출한다. 모듈 내부에서는 비공개 구현 이름도 사용할 수 있다. 변환 뒤 저장하는 동작을 재사용하므로 나중에 생성 규칙을 바꿀 때 확인할 위치가 줄어든다.

add_vectors는 성분별 합을 반환한다. 두 스칼라 곱셈 함수 중 실제 곱셈을 수행하는 것은 scale_left다. scale_right의 v = scale_left(s, a)는 결과 벡터를 같은 타입의 변수에 복사하는 기본 대입이다. 실수 배열용 정의 대입은 여기에 적용되지 않는다.

dot_vectors의 결과는 real(wp)다. 연산 결과의 타입은 사용한 기호만으로 결정되지 않고 구현 함수의 결과 선언에 따른다. 내적을 벡터 변수에 넣으려 하면 이 모듈에는 실수에서 벡터로 대입하는 규칙이 없으므로 오류가 된다.

계산 함수에 붙은 pure는 외부 상태를 바꾸는 동작을 제한한다. 이 함수들은 입력을 읽고 결과만 만든다. 사용자 연산자를 쓰기 위해 모든 구현이 반드시 순수해야 하는 것은 아니지만, 수식 안에서 호출하는 계산에는 이런 제한이 설계를 명료하게 한다.

assign_from_array는 원소 개수를 먼저 검사한 뒤 성분을 읽는다. 검사를 뒤에 두면 길이가 부족한 배열에서 범위를 벗어난 원소를 먼저 참조할 수 있다. 가정 형상 더미 인자 rhs(:)는 여기서 하한이 1이므로 rhs(1)과 rhs(2)는 전달된 배열의 첫째와 둘째 원소를 뜻한다.

주 프로그램의 use ... only:에는 연산자와 assignment(=)를 명시한다. 타입만 가져오면 모듈에 정의한 모든 연산이 함께 들어온다고 생각하면 안 된다. 선택적으로 가져오는 목록에 실제로 사용할 인터페이스를 포함한다.

기울기의 가로 성분은 (40 - 80) / 1이므로 -40이다. 세로 성분은 (70 - 50) / 1이므로 20이다. 열전도율에 음수를 붙여 곱하면 열 흐름은 (80, -40)이 된다. (-conductivity)는 실수의 부호를 바꾸는 기존 연산이며, 그 뒤의 곱셈만 벡터용 정의를 사용한다.

combined 계산은 벡터를 먼저 0.5배 하고 원래 벡터를 더한다. axis = make_vector(1, 0)는 정수용 생성 경로를 확인한다. 이 축 벡터와 열 흐름의 내적은 열 흐름의 가로 성분이다. 마지막 출력에서는 실수 필드 폭과 소수 자릿수를 지정해 실행 환경의 기본 출력 형식에 의존하지 않는다.

실행 결과

모듈을 주 프로그램보다 앞에 놓았으므로 파일 하나를 다음 명령으로 컴파일한다. 출력 파일 이름을 명시하면 macOS와 Linux에서 같은 실행 명령을 사용할 수 있다.

gfortran -std=f2018 -Wall main.f90 -o plate_vectors
./plate_vectors

예상 출력은 다음과 같다. 각 성분은 소수점 아래 두 자리까지 표시한다.

gradient:  -40.00   20.00
flux:       80.00  -40.00
offset:      0.25   -0.50
combined:  120.00  -60.00
flux dot gradient:  -4000.00
estimated temperature:   40.00
x projection:   80.00

열 흐름과 기울기의 내적은 80 × (-40) + (-40) × 20이므로 -4000이다. 변위와 기울기의 내적은 -20이므로 중심 온도 60에서 추정한 온도는 40이다. 이 값은 한 지점의 기울기를 이용한 추정 결과이며, 실제 판 전체의 온도 분포를 계산한 결과와 구별해야 한다.

실무에서 자주 틀리는 것

실수 종류와 혼합 인자를 자동으로 맞춰 준다고 생각한다

다음 호출은 예제에서 제공한 어느 생성 함수에도 맞지 않는다. 첫째 인자는 예제 실수 종류이고 둘째 인자는 정수다. 제네릭은 두 인자를 각각 다른 구현에 나누어 보내지 않는다.

! 잘못된 코드
gradient = make_vector(1.0_wp, 0)

둘 다 실수로 전달하거나 둘 다 정수로 전달한다. 혼합 호출을 실제로 자주 사용한다면 그 인자 구성의 구현을 별도로 제공하는 선택도 가능하다.

! 고친 코드
gradient = make_vector(1.0_wp, 0.0_wp)
axis = make_vector(1, 0)

make_vector(1.0, 0.0)도 두 실수가 wp와 다른 종류이면 일치하지 않는다. 숫자의 값이 같다는 것과 인터페이스에 맞는 종류라는 것은 별개의 조건이다.

한쪽 곱셈만 정의하고 양쪽 순서를 사용한다

실수와 벡터를 받는 scale_left만 등록했다면 다음 코드의 곱셈은 지원되지 않는다. 교환법칙은 수학적 성질이며 인터페이스 등록을 대신하지 않는다.

! scale_left만 등록한 경우의 잘못된 코드
combined = flux * 0.5_wp

그 인터페이스만 쓸 계획이면 피연산자 순서를 맞춘다. 양쪽 표기가 필요하면 완성 코드처럼 scale_right도 등록한다.

! 고친 코드
combined = 0.5_wp * flux

정수 곱셈도 같은 주의가 필요하다. 현재 모듈의 2 * flux는 정수와 벡터의 조합이라 지원되지 않는다. 2.0_wp * flux로 쓰거나 정수용 곱셈 함수를 추가한다.

사용자 정의 연산자의 우선순위를 내적 기호의 의미로 짐작한다

다음 식은 사람이 의도한 내적을 먼저 계산하지 않는다. 사용자 정의 이항 연산자보다 덧셈의 우선순위가 높기 때문이다.

! 잘못된 코드
estimate = center + gradient .dot. offset

내적의 범위를 괄호로 묶는다. 계산이 길어지면 내적 결과를 실수 변수에 먼저 저장하는 것도 읽기 쉬운 방법이다.

! 고친 코드
estimate = center + (gradient .dot. offset)

연산자 이름을 .dot.로 정했다고 컴파일러가 수학에서 사용하는 내적의 우선순위를 부여하는 것은 아니다. 사용자 정의 이항 연산자는 언어가 정한 우선순위를 따른다.

정의 대입의 양쪽 인자 의도를 뒤집는다

대입 구현은 결과를 함수로 반환하지 않는다. 첫 번째 더미 인자를 통해 왼쪽 객체를 설정한다. 첫 번째 인자를 입력 전용으로 선언하면 성분에 값을 저장할 수 없다.

! 잘못된 선언과 저장
type(vector2), intent(in) :: lhs
real(wp), intent(in) :: rhs(:)

lhs%x = rhs(1)

완성 코드에서는 기존 벡터 값을 읽을 필요가 없으므로 왼쪽을 출력 인자로 선언한다. 두 원소를 확인하는 검사도 성분 저장보다 먼저 둔다.

! 고친 선언과 저장
type(vector2), intent(out) :: lhs
real(wp), intent(in) :: rhs(:)

if (size(rhs) /= 2) then
  error stop 'vector assignment requires two elements'
end if
lhs%x = rhs(1)
lhs%y = rhs(2)

정의 대입의 왼쪽 인자에는 상황에 따라 intent(inout)도 사용할 수 있다. 기존 값을 읽지 않고 전체 성분을 채우는 이번 구현에서는 intent(out)이 의도를 더 직접적으로 드러낸다.

한눈에 보기

인터페이스 표기와 구현 프로시저의 역할
공개 표기구현 형태인자와 결과확인할 규칙
make_vector함수 두 개실수 두 개 또는 정수 두 개 → 벡터인자 특성으로 구별한다
a + b함수벡터 두 개 → 벡터성분별로 더한다
s * a, a * s함수 두 개실수와 벡터 → 벡터양쪽 순서를 각각 등록한다
a .dot. b함수벡터 두 개 → 실수복합 식에서 괄호를 확인한다
a = values서브루틴왼쪽 벡터, 오른쪽 실수 배열왼쪽을 설정하고 크기를 검사한다

좋은 연산자 정의는 표기와 실제 계산이 일치한다. 벡터 덧셈은 성분별 합, 스칼라 곱셈은 같은 배율의 적용, 내적은 실수 반환이라는 기대를 지킨다. 기호를 추가하기 전에 인자 타입, 결과 타입, 피연산자 순서와 단위를 먼저 정하면 사용자가 수식을 해석하기 쉽다.

이번 타입은 벡터의 두 성분을 묶지만 온도 기울기와 열 흐름의 물리적 단위까지 구별하지는 않는다. 따라서 타입이 같다는 이유만으로 두 값을 더하는 것이 물리적으로 타당해지는 것은 아니다. 연산자가 제공하는 자료 구조의 규칙과 계산 모델의 규칙을 함께 확인해야 한다.

다음 장에서는 배열을 다루는 표현을 넓힌다. 이번 장에서 배열은 벡터를 만드는 입력 표현으로만 사용했다. 벡터 하나에 대한 인터페이스를 먼저 안정시키면, 이후 여러 지점의 자료를 배열로 관리할 때도 개별 계산의 의미를 유지할 수 있다.

연습 문제

  1. 완성 코드의 공개 인터페이스만 사용해 벡터 a = (3, 4)를 만들고, a .dot. a와 그 제곱근을 출력하라. 두 출력에 소수점 아래 두 자리를 사용하라.
  2. 벡터 뺄셈을 추가하라. 구현 함수 이름은 subtract_vectors로 하고, make_vector(5, 2) - make_vector(1, 3)의 결과를 구하라. 모듈에서 추가할 선언과 함수 본체를 제시하라.
  3. make_vector에 real(wp) 일차원 배열을 받는 생성 함수를 추가하라. 배열의 원소 개수가 두 개인지 검사하고, 완성 코드의 정의 대입을 재사용하라. 배열 길이 2와 길이 3을 받는 함수를 각각 등록하는 방식이 적절하지 않은 이유도 설명하라.
  4. 열전도율을 0.5로 바꿀 때 열 흐름과 기울기의 내적을 계산하라. 기울기를 g, 양의 열전도율을 k라고 할 때 내적이 양수가 되지 않는 이유를 식으로 설명하라.

정답과 해설

1. 자기 자신과의 내적

다음 선언을 주 프로그램의 선언부에, 계산과 출력을 실행부에 추가한다. 정수 두 개를 전달하므로 정수용 생성 함수가 선택된다.

! 선언부
type(vector2) :: a
real(wp) :: squared_length

! 실행부
a = make_vector(3, 4)
squared_length = a .dot. a
write(*, '(a,f8.2)') 'squared length:', squared_length
write(*, '(a,f8.2)') 'length:', sqrt(squared_length)

자기 자신과의 내적은 두 성분의 제곱합이다. 따라서 9와 16의 합인 25를 얻고, 제곱근은 5다.

squared length:   25.00
length:    5.00

2. 벡터 뺄셈

다음 공개 선언과 인터페이스를 모듈의 contains 앞에 추가한다.

public :: operator(-)

interface operator(-)
  module procedure subtract_vectors
end interface

함수는 contains 뒤에 추가한다. 덧셈과 같은 인자 구성을 사용하고 각 성분의 차이를 반환한다.

pure function subtract_vectors(a, b) result(v)
  type(vector2), intent(in) :: a, b
  type(vector2) :: v

  v%x = a%x - b%x
  v%y = a%y - b%y
end function subtract_vectors

결과는 (4, -1)이다. 주 프로그램이 only: 목록을 사용하므로 그 목록에 operator(-)도 추가해야 한다. 이 함수는 두 인자를 받으므로 단항 음수 연산까지 제공하지는 않는다.

3. 배열을 받는 생성 함수

make_vector 인터페이스에 다음 등록을 추가한다. 기존 함수는 인자 두 개를 받고 새 함수는 일차원 배열 하나를 받으므로 구별할 수 있다.

module procedure vector_from_array

함수 본체는 다음과 같다. 배열 크기 검사는 이미 정의 대입에 있으므로 같은 검사를 다시 작성하지 않는다.

function vector_from_array(values) result(v)
  real(wp), intent(in) :: values(:)
  type(vector2) :: v

  v = values
end function vector_from_array

이 함수는 순수 프로시저로 선언하지 않는다. 재사용하는 assign_from_array가 순수 프로시저가 아니기 때문이다. 호출은 a = make_vector([3.0_wp, 4.0_wp])처럼 쓴다. 배열 길이만 다른 두 함수는 인자의 타입, 종류와 차원 수가 같아서 제네릭의 구별 근거가 되지 않는다.

4. 열 흐름 방향 확인

기울기는 여전히 (-40, 20)이다. 열전도율이 0.5이면 열 흐름은 (20, -10)이고, 기울기와의 내적은 20 × (-40) + (-10) × 20 = -1000이다.

열 흐름을 q = -k g라고 쓰면 q · g = -k (gx² + gy²)다. 제곱합은 음수가 아니고 k가 양수이므로 결과는 양수가 될 수 없다. 기울기가 영벡터이면 0이고, 그 밖에는 음수다. 예제에서 계산한 음의 내적은 열 흐름의 방향이 온도 증가 방향과 반대라는 해석과 일치한다.

댓글 0

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

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