使用Fortran设计相互交互的对象的建议

7

下午我一直在尝试解决这个问题,但一直没有头绪,希望有人能够帮助我。

我有一个抽象基类,叫做 base_model (例如),在Fortran2003中看起来像是:

type, abstract :: base_model
contains
  procedure(initMe), pass(this), deferred :: init ! constructor
  procedure(delMe), pass(this), deferred :: delete ! destructor
  procedure(solveMe), pass(this), deferred :: solve
end type base_model

这里,显然使用了抽象接口块定义了 initMe, delMesolveMe 三个抽象过程。接下来我有三个派生类,分别叫做 model1, model2model3 (假设):

type, extends(base_model) :: model1
  double precision :: x,y
contains
  procedure :: init => init_model1
  procedure :: delete => delete_model1
  procedure :: solve => solve_model1
end type model1

type, extends(base_model) :: model2
contains
  procedure :: init => init_model2
  procedure :: delete => delete_model2
  procedure :: solve => solve_model2
end type model2

type, extends(base_model) :: model3
contains
  procedure :: init => init_model3
  procedure :: delete => delete_model3
  procedure :: solve => solve_model3
end type model3

我随后有一个“控制”对象,称为control(假设),它扩展了一个抽象的base_control对象:
type, abstract :: base_control
  class(base_model), allocatable :: m1
  class(base_model), allocatable :: m2
  class(base_model), allocatable :: m3
contains
  procedure(initMe), pass(self), deferred :: init
  procedure(delMe), pass(self), deferred :: delete
  procedure(runMe), pass(self), deferred :: run
end type base_control

type, extends(base_control) :: control
contains
  procedure :: init => init_control
  procedure :: delete => delete_control
  procedure :: run => run_control
end type control

对象m1、m2和m3可以分配到任何模型中:model1、model2或model3,并且根据用户请求的“控制”,以任意特定顺序“解决”。
这三个可分配对象(m1、m2和m3)需要彼此传递数据。考虑到它们是“控制”对象的成员,我可以为每个模型定义一个“getter”,然后将所需数据传递到每个模型中。但是,具体的模型在编译时是未知的,因此,“控制”对象不知道要获取什么数据,实际上,模型也不知道要接收什么数据!
例如,如果我分配model1::m1(即将m1分配为model1类型),则它将包含两个数据位double precision :: x,y。然后,如果将m2分配为model2(allocate(model2::m2)),它可能需要x,但是如果将其分配为model3(allocate(model3::m2)),则它可能需要从m1获取y。因此,鉴于“控制”对象无法知道m2分配的类型,如何从m1获取必要的数据以传递到m2中?
另一个复杂之处在于模型之间的交互通常是循环的。也就是说,m1需要从m2获取数据,m2需要从m1获取数据等等。而且,所需的数据通常不仅特定于模型,而且类型和数量也是可变的。
不幸的是,数据x和y不是base_model的成员,因此将m1作为参数传递给m2也行不通。
因此,我有以下问题:
  1. 有没有更好的设计方式,使得这些对象之间可以轻松地传递数据?在这里查看后,有一些建议是重新设计对象,使它们之间的交互不是循环引用。然而,在这里那种设计是必要的!

  2. 我需要为每一个可能在对象之间共享的数据编写一个"getter"吗?这似乎需要写很多代码(我有很多可能会被共享的数据)。但是,这也似乎相当复杂,因为"getter"(特定于某个数据)还必须满足抽象接口。

在像Python这样的高级语言中,这很容易,因为我们可以简单地创建一个新的数据类型作为模型的组合,但据我所知,在Fortran中这是不可能的。

提前感谢您的帮助,非常感谢。

Edit: 经与francescalus讨论后,select type是一个选项。确实,在上面给出的简单示例中,select type是一个很好的选择。然而,在我的实际代码中,这将导致大量嵌套的select type,因此如果有一种不使用select type的方法,我更喜欢它。感谢francescalus指出我关于select type的错误。


1
你的“模型组合”会是什么样子?你已经排除了“选择类型”吗? - francescalus
好问题,我想“模型的组合”只是数据的一个副本。我想,在Python中,model1.init(self, model2)应该包含类似于self.m2 = model2的语句。虽然我必须承认,我不知道确切的结构。但是,我已经看到了在Python中通过组合对象创建数据类型的示例。不幸的是,在Fortran中,从m1的角度来看,m2是基础类型。因此,它无法访问对象的成员。选择类型也有同样的问题。虽然它可以确定派生类型,但无法访问其数据。 - crispyninja
一旦您选择了类型,您就会拥有一个声明类型的东西,m2 的动态类型和组件是自由可访问的。(我可能没有跟上您的思路;即使是显示所需内容的“错误”代码也更容易解析。) - francescalus
好的,我对选择类型的理解有误 - 你是正确的。在我的测试用例中使用它的方式是错误的。即便如此,在我的实际代码中,大量的排列组合将导致一个非常庞大和嵌套的选择类型 - 因此,如果通过重新设计对象来找到更好的方法,那么我更愿意采用这种方式。 - crispyninja
感谢francescalus指出我的选择类型错误。我已经在我的帖子中添加了一个编辑,反映了我们的讨论。 - crispyninja
你能详细说明一下你的编辑吗:什么是大量嵌套的select_types?什么是大量,你能给一个例子吗?同时要考虑到“痛苦守恒定律”,你需要将代码放在某个地方。 - kvantour
2个回答

3
为了回答你的两个问题: 是否有更好的设计方法? 我不太清楚为什么您的设计中有这么多限制,但简而言之是的,您可以为您的模型使用上下文管理器。我建议您查看这个答案: Context class pattern 每个模型都必须编写getter方法吗? 不完全是。如果您在这个特定问题上使用上下文策略,您唯一需要在每个模型上实现的是一个setter方法,它将在模型之间共享数据。
我在Python中为这种情况实现了一个可行的解决方案。代码胜过语言。我避免使用任何Python的特殊功能,以便为您提供清晰的理解如何在这种情况下使用上下文。
from abc import ABC, abstractmethod
import random

class BaseModel(ABC):
    def __init__(self, ctx):
        super().__init__()
        self.ctx = ctx
        print("BaseModel initializing with context id:", ctx.getId())

    @abstractmethod
    def solveMe():
        pass

class BaseControl(object):
    # m1 - m3 could be replaced here with *args
    def __init__(self, m1, m2, m3):
        super().__init__()
        self.models = [m1, m2, m3]


class Control(BaseControl):
    def __init__(self, m1, m2, m3):
        super().__init__(m1, m2, m3)

    def run(self):
        print("Now Solving..")
        for m in self.models:
            print("Class: {} reports value: {}".format(type(m).__name__, m.solveMe()))


class Model1(BaseModel):
    def __init__(self, x, y, ctx):
        super().__init__(ctx)
        self.x = x
        self.y = y
        ctx.setVal("x", x)
        ctx.setVal("y", y)

    def solveMe(self):
        return self.x * self.y

class Model2(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("x")

class Model3(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("y")

class Context(object):
    def __init__(self):
        self.modelData = {}
        self.ctxId = random.getrandbits(32)

    def getVal(self, key):
        return self.modelData[key]

    def setVal(self, key, val):
        self.modelData[key] = val

    def getId(self):
        return self.ctxId


ctx = Context()

m1 = Model1(1,2, ctx)
m2 = Model2(4, ctx)
m3 = Model3(6, ctx)

# note that the order in the arguments to control defines behavior
control = Control(m1, m2, m3)
control.run()

输出

python context.py
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
Now Solving..
Class: Model1 reports value: 2
Class: Model2 reports value: 4
Class: Model3 reports value: 12

解释

简而言之,我们创建一个上下文类,其中包含一个可以在不同模型之间共享的字典。这种实现非常特定于您提供的原始数据类型(即x、y、z)。如果您需要在将数据共享到模型之前计算数据,则仍然可以使用此模式,只需将solveMe()的返回值替换为延迟承诺。


这个怎么用Fortran实现呢? - francescalus
我不太了解Fortran,无法回答这个问题。因为这更像是一个架构问题,所以我认为用Python示例来回答比较合适,但会排除任何特定于Python的功能,如装饰器。 - Josue Alexander Ibarra
1
如果我们使用某种字典类型(例如从网络上获取的)并以某种方式实现此行(“self.modelData = {}”),那么我们可以相当直接地将Python代码翻译成Fortran。 - roygvib

2

FWIW,以下是一种类似的尝试,基于键/值访问另一个对象的字段(*)。为了简单起见,主程序从child1_t对象获取一个整数,并将复杂值设置为child2_t对象(这两个对象都是parent_t的扩展类型)。

parent.f90:

module parent_m
    implicit none

    type, abstract :: parent_t   !(abstract is optional)
    contains
        procedure :: set
        procedure :: get
        procedure :: show
    endtype

    type composite_t
        class(parent_t), allocatable :: pa, pb
    endtype

contains
    subroutine set( this, key, val )  ! key-based setter
        class(parent_t), intent(inout) :: this
        character(*),   intent(in)     :: key
        integer,        intent(in)     :: val
    endsubroutine

    subroutine get( this, key, val )  ! key-based getter
        class(parent_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val
    endsubroutine

    subroutine show( this )   ! print contents
        class(parent_t), intent(in) :: this
    endsubroutine
end module

child.f90:

module child_m
    use parent_m, only: parent_t
    implicit none

    type, extends(parent_t) :: child1_t
        integer :: n1 = 777   ! some property
    contains
        procedure :: get  => child1_get
        procedure :: show => child1_show
    endtype

    type, extends(parent_t) :: child2_t
        complex :: c2 = ( 0.0, 0.0 )   ! another property
    contains
        procedure :: set  => child2_set
        procedure :: show => child2_show
    endtype

contains

    subroutine child1_get( this, key, val )
        class(child1_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val

        select case( key )
            case ( "num", "n1" ) ; val = this % n1  ! get n1
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child1_show( this )
        class(child1_t), intent(in) :: this
        print *, "[child1] ", this % n1
    endsubroutine

    subroutine child2_set( this, key, val )
        class(child2_t), intent(inout) :: this
        character(*),    intent(in)    :: key
        integer,         intent(in)    :: val

        select case( key )
            case ( "coeff", "c2" ) ; this % c2 = cmplx( real(val), 0.0 )  ! set c2
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child2_show( this )
        class(child2_t), intent(in) :: this
        print *, "[child2] ", this % c2
    endsubroutine

end module

main.f90:

program main
    use parent_m, only: composite_t
    use child_m,  only: child1_t, child2_t
    implicit none
    type(composite_t) :: c
    integer itmp

    allocate( child1_t :: c % pa )
    allocate( child2_t :: c % pb )

    print *, "initial state:"
    call c % pa % show()
    call c % pb % show()

    call c % pa % get( "num",  itmp )   ! get an integer value from pa
    call c % pb % set( "coeff", itmp )  ! set a complex value to pb

    print *, "modified state:"
    call c % pa % show()
    call c % pb % show()
end

编译和结果:

 $ gfortran parent.f90 child.f90 main.f90

 initial state:
 [child1]          777
 [child2]              (0.00000000,0.00000000)
 modified state:
 [child1]          777
 [child2]              (777.000000,0.00000000)

尽管上述代码仅处理整数和复数的“数据传输”,但其他类型的数据可以类似地添加到“select case”结构中(无需为每种类型添加新的getter/setter方法)。此外,如果必要,我们可以通过“generic”关键字(在“parent_t”类型中)对不同类型的值重载“get()”和“set()”(例如,“set_int()”和“set_real()”),并在扩展类型中覆盖它们。同样适用于数组类型的值,也许...
如果在扩展类型之间进行数据(复制)转移很昂贵(例如,大型数组),则我认为getter可以返回指向它们的指针,以便“childX_t”可以以紧密耦合的方式通信(而不知道其实现方式)。
(但是,我认为可能有比上述做法更简单的方法,包括问题中的第1点(例如,重新设计程序本身)。此外,如果我们手头有一些字典类型,则使用字典(如另一个答案)对我来说更具吸引力。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接