태그 보관물: OOP

포트란과 런타임 다형성 (runtime polymorphism)

포트란에서의 객체지향프로그래밍 다형성과 관련하여 함수 오버로딩연산자 오버로딩에 대해 글을 올린 적이 있습니다. 함수 오버로딩은 다른 함수를 같은 이름으로 사용하는 것으로, 이를 통해 하나의 함수 이름으로 다른 인수(argument)를 사용하는 것과 같은 효과를 나타낼 수 있습니다. 연산자 오버로딩은 함수를 이용해 기존 연산자의 기능을 재정의하거나 새로운 연산자를 만드는 기능이었죠. 런타임 다형성은 하나의 함수에 서로 다른 인수를 사용하는 기능으로, 상속 및 오버라이딩과 관련이 있습니다.

함수 오버로딩에서도 하나의 함수 이름으로 다른 인수를 사용하는 것과 같은 효과를 낼 수 있다고 하였는데, 실제로는 인수에 따라 다른 함수들을 정의하고 같은 이름으로 사용한 것인 반면에, 런타임 다형성을 이용하면 하나의 함수만 정의하고 서로 다른 인수를 사용할 수 있습니다. 이 때 인수들은 아무 인수나 되는 것은 아니고 서로 상속 관계에 있어야 합니다. 코드를 살펴보겠습니다.

module m_hero
    type:: hero_t
    contains
        procedure::speak => speak_hero
    end type 

    type,extends(hero_t):: ironman_t
    contains
        procedure:: speak => speak_ironman
    end type

    type,extends(hero_t):: spiderman_t
    contains
        procedure:: speak => speak_spiderman
    end type

contains

    subroutine speak_hero(hero)
    class(hero_t):: hero
    print*,'I am a hero.'
    end subroutine

    subroutine speak_ironman(hero)
    class(ironman_t):: hero
    print*,'I am Iron Man.'
    end subroutine

    subroutine speak_spiderman(hero)
    class(spiderman_t):: hero
    print*,"I'm Peter."
    end subroutine

    subroutine who_are_you(hero)
    class(hero_t):: hero
    call hero%speak()
    end subroutine

end module

위 코드에서 ironman_tspiderman_thero_t를 상속합니다. 모두 각자의 speak 메소드를 가지고 있는데, 서로 다른 type이지만 다음과 같이 who_are_you 서브루틴의 인수로 사용할 수 있습니다.

program test_runtime_polymorphism1
    use m_hero
    type(hero_t):: hero
    type(ironman_t):: ironman
    type(spiderman_t):: spiderman

    call who_are_you(hero)
    call who_are_you(ironman)
    call who_are_you(spiderman)
end program

실행 결과는 예상대로 다음과 같습니다.

I am a hero.
I am Iron Man.
I’m Peter.

이번에는 ihero라는 변수 값에 따라 hero들 중 한 명이 달려가 사람들을 구해야 한다고 생각해봅시다(모두 run, save_people 메소드를 가지고 있다고 합시다). 아래와 같이 구현할 수 있습니다.

program test_runtime_polymorphism2_not_good
    use m_hero
    type(ironman_t):: ironman
    type(spiderman_t):: spiderman
    integer:: ihero = 1

    select case (ihero)
    case (1)
        call who_are_you(ironman)
        !call ironman%run()
        !call ironman%save_people()
    case (2)
        call who_are_you(spiderman)
        !call spiderman%run()
        !call spiderman%save_people()
    end select
end program

그러나 위 코드는 중복이 많습니다. Hulk, Black Widow, Thor 등 새로운 avenger가 올 때마다 코드가 여러 줄씩 늘어나게 됩니다. 아름답지 않습니다! 포인터를 이용하면 hero_t 클래스 변수가 자식 type을 가리키도록 할 수 있습니다.

program test_runtime_polymorphism2_better
    use m_hero
    class(hero_t),pointer:: hero
    type(ironman_t),target:: ironman
    type(spiderman_t),target:: spiderman
    integer:: ihero = 1

    select case (ihero)
    case (1)
        hero => ironman
    case (2)
        hero => spiderman
    end select

    call who_are_you(hero)
    !call hero%run()
    !call hero%save_people()
end program

아름답습니다! 유지 보수하기 훨씬 좋아졌습니다.

Polymorphic Fortran & C

포트란 함수 오버로딩과 중복

앞서 포트란 함수 오버로딩에 관한 글을 올렸습니다. 포트란 모듈과 인터페이스를 이용하면 서로 다른 자료형을 인자로 받는 함수나 서브루틴이라도 같은 이름으로 사용할 수가 있었습니다. 그런데, 동적 자료형을 지원하는 언어와 달리, 포트란에서는 자료형별로 서브루틴들을 따로 만든 후에 같은 이름으로 호출하였습니다. 동적 자료형 언어에서는 함수 자체를 한 번만 작성하면 되지요. 여기에서 포트란 코드 작성에 중복이 발생하게 됩니다. 이러한 중복을 제거하기 위해 파이썬으로 만든 스크립트가 polyfc (Polymorphic Fortran & C)입니다. 예전에 Forpedo에서 아이디어를 얻었는데, Forpedo를 사용하려니 좀 복잡해서 Python 연습도 할 겸 gpl용으로 만들었습니다.

예제

이해하기 쉽게 예제를 살펴보겠습니다. 배열의 내용을 출력하는 서브루틴을 작성하려고 합니다. 배열은 integer, real, real(kind=8), complex, complex(kind=8) 다섯 종류의 자료형을 지원하려고 합니다. 그럼 서브루틴을 자료형에 따라 총 5개를 작성해야 하는데, 선언부만 다르고 나머지는 동일하거나 거의 비슷하게 됩니다. 그래서 일종의 템플릿 서브루틴을 만들고, 필요한 부분만 바꿔가며 서브루틴을 복제하려고 합니다. 그러면 거의 비슷한 코드를 중복해서 작성하는 수고를 덜 수 있겠죠. 아래는 polyfc에 입력으로 들어가는 템플릿 파일입니다.

module polyfc_example

!@interface print_array
contains
!@template print_array ifdcz
    subroutine print_array_<name>(arr)
    <type>,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
!@end
end module

포트란 주석을 이용하여 인터페이스가 들어갈 부분과 템플릿 부분을 표시하고, 자료형에 따라 바뀌어야 하는 부분은 <name>, <type>으로 표시하였습니다. 이 외에 필요할 경우 <kind><esize>도 지원합니다. 바뀌며 들어가는 부분은 다음 표를 보면 알 수 있습니다.

Name Fortran type C type Fotran kind esize
i integer int (kind=4) 4
f real float (kind=4) 4
d real(kind=8) double (kind=8) 8
c complex float complex (kind=4) 8
z complex(kind=8) double complex (kind=8) 16
b logical (not supported) (kind=4) 4
s character(len=*) char (len=*) 1

그래서 polyfc input.f90 > output.f90과 같이 실행했을 때 얻게 되는 파일은 다음과 같습니다.

! This file was generated from pfc.example.f90 by Polyfc at Mon Jul 28 22:12:32 2014.
! Do not edit this file directly.

module polyfc_example

    interface print_array
        module procedure print_array_i
        module procedure print_array_f
        module procedure print_array_d
        module procedure print_array_c
        module procedure print_array_z
    end interface print_array

contains

    subroutine print_array_i(arr)
    integer,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_f(arr)
    real,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_d(arr)
    real(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_c(arr)
    complex,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_z(arr)
    complex(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
end module

자동으로 작성하면 손으로 복사했을 경우 생길 수 있는 오류도 피할 수 있겠죠. 단, 코드에 버그가 있을 때에는 output 파일에서 버그가 있는 곳을 찾아 input 파일의 해당 위치를 고쳐줘야 합니다.

참고로, polyfc 이름이 의미하듯, C도 지원합니다. 단, C에서는 <name><type>만 지원합니다. 물론 인터페이스도 지원하지 않습니다. C에서는 아래와 같은 방식으로 사용 가능합니다.

//@template ifdczbs
void abc_<name>(<type> def)
{
    ...
}
//@end

포트란과 연산자 오버로딩

함수 오버로딩과 마찬가지로, 연산자 오버로딩도 객체지향 프로그래밍의 다형성과 관련된 개념입니다. 포트란 90에서 연산자 오버로딩을 사용하는 방법을 살펴보겠습니다. 아래 코드는 이차원 좌표 자료형과 두 개의 점을 더하는 함수 예제입니다.

module point2d_op

type point
    real x, y
end type

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3=add(p1,p2)

print*, p3%x,p3%y
end program

새로운 자료형을 정의했으니 새로운 자료형에 대응하는 더하기 연산도 따로 정의할 필요가 있습니다. 그런데 p3=add(p1,p2)와 같이 쓰는 것보다는 p3=p1+p2로 쓰는 것이 더 직관적이고 이해하기 쉽겠죠. 이 때 사용하는 것이 연산자 오버로딩입니다.

연산자 오버로딩

포트란에서는 interface문을 사용하여 이미 존재하는 연산자를 오버로드 하거나 새로운 연산자를 정의할 수 있습니다. 아래 예제는 + 연산자를 오버로드 하는 예제입니다.

module point2d_op

type point
    real x, y
end type

interface operator (+) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 + p2

print*, p3%x,p3%y
end program

수학 연산자를 오버로딩할 때에는 수학적 정의에 합당하도록 또는 이해하기 쉽도록 정의하는 것이 좋습니다. 위의 add 함수를 - 연산자에 오버로딩하는 것도 가능하나 그렇게 되면 프로그래밍할 때 문제가 발생하겠죠.

새로운 연산자 정의

연산자를 오버로드하지 않고 새로 정의할 수도 있습니다. 새로 정의하는 연산자는 .name.과 같이 '.'으로 시작해서 '.'으로 끝나야 합니다. 아래 예제는 add 함수를 .add. 연산자로 정의한 경우입니다. p3=add(p1,p2) 대신 p3=p1.add.p2와 같이 사용한 것을 볼 수 있습니다.

module point2d_op

type point
    real x, y
end type

interface operator (.add.) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 .add. p2

print*, p3%x,p3%y
end program

포트란과 함수 오버로딩

포트란과 객체지향 프로그래밍

포트란이라 하면 옛날 언어라고 생각하기 쉬운데, 포트란 언어도 시대의 흐름에 따라 계속 발전해 오고 있습니다. 특히, 2003버전부터는 클래스와 상속 등을 지원하는 객체지향 프로그래밍 언어라 할 수 있습니다. 안타깝게도, 한글로 된 포트란 서적이 별로 없고, 출판된 것도 95버전까지밖에 안 나와 있어서 앞으로 블로그에서 몇 가지 객체지향 프로그래밍과 관련된 기능들을 소개하고자 합니다.

포트란과 함수 오버로딩

함수 오버로딩이란 같은 이름의 서브프로그램(서브루틴, 함수, 메소드 등)이 인자에 따라 다른 기능을 하는 것을 말합니다. 객체지향 프로그래밍의 다형성과 관련된 개념인데, 포트란에서는 90버전에 도입된 interface문을 이용하여 적용 가능합니다.

아래 예제는 2차원 좌표라는 자료형을 정의하고 좌표들 사이의 거리를 구하는 함수를 구현한 예제입니다. 자료형을 정의할 때 single precision 자료형 point_sp와 double precision 자료형 point_dp 두 가지를 정의했고, 따라서 함수도 single precision용 distance_sp와 double precision용 distance_dp 두 가지를 정의했습니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance_sp(s1,s2)
dist_d=distance_dp(d1,d2)

print*, dist_s, dist_d
end program

위의 예제에서 두 개의 함수가 하는 일은 같지만, 정적 자료형 언어의 특성상 자료형에 따라 두 개의 함수를 정의해야 했습니다. 이 프로그램은 간단해서 별 문제가 없지만, 프로그램이 복잡해지면 이렇게 같은 기능의 다른 함수들로 인해 인터페이스가 복잡해지게 됩니다. 같은 기능의 함수들을 single/double과 같은 자료형에 상관없이 같은 이름으로 사용할 수 있다면 프로그래밍 인터페이스가 좀 더 단순해지겠죠. 위의 모듈에 아래의 interface 구문을 넣으면 distance_sp 함수와 distance_dp 함수를 모두 distance라는 이름으로 사용할 수 있습니다.

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

위와 같이 선언해 놓으면 두 함수의 인자가 두 개의 point_sp와 두 개의 point_dp로 다르기 때문에 인자의 자료형을 가지고 어떤 함수를 사용해야하는지 판단할 수 있게 됩니다. 만약 두 함수의 인자가 동일하다면 interface문을 사용할 수 없습니다. 아래는 함수 오버로딩을 사용하도록 수정한 예제입니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance(s1,s2)
dist_d=distance(d1,d2)

print*, dist_s, dist_d
end program

자료형에 상관없이 distance라는 함수를 이용하여 거리를 계산하고 있음을 알 수 있습니다. 물론 위에서 distance_spdistance_dp도 사용 가능합니다. 함수 오버로딩을 사용한다고 프로그램의 실행 속도가 빨라지지는 않습니다. 하지만 서브프로그램들의 인터페이스를 단순화하여 프로그래밍을 좀 더 편하게 할 수 있다는 장점이 있습니다.