I have been banging my head against a wall all afternoon trying to figure out this problem, so I hope someone can help me.
I have an abstract base class, called base_model
(say), which in Fortran2003 looks like:
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
where, obviously, the abstract procedures initMe
, delMe
and solveMe
are defined using an abstract interface block. I then have three derived classes, called model1
, model2
and model3
(say):
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
I then have a "controlling" object, called control
(say), which extends an abstract 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
The objects m1
, m2
and m3
can be allocated into any of the models: model1
, model2
or model3
, and are "solved" in any particular order depending on which "control" is requested by the user.
The three allocatable objects (m1
, m2
and m3
) need to pass data between them. Given that they are members of a "controlling" object, I can define a "getter" for each model which then passes the required data into each model. However, the specific models are not known at compile time and therefore, the "control" object does not know what data to get and indeed, the models don't know what data to receive!
For example, if I allocate(model1::m1)
(that is, allocate m1 to be of type model1
) then it will contain two bits of data double precision :: x,y
. Then if m2
is allocated to be of type model2
(allocate(model2::m2)
), it could require x
but if it is allocated to be of type model3
(allocate(model3::m2)
) then it may require y
from m1
. Therefore, given that the "controlling" object cannot know what type m2
is allocated to be, how can it get the necessary data from m1
to pass into m2
?
An additional complication is that the interactions between the models are in general, circular. That is, m1
requires data from m2
, m2
requires data from m1
and so-on. Also, the data required is in general not only specific to the models but also variable in both type and quantity.
Unfortunately, the data x
and y
are not members of base_model
and therefore, passing m1
into m2
as an argument would not work either.
So I have the following questions:
Is there a better way to design these objects so that I can pass data between them easily? Looking around on here, there have been some suggestions that the best thing to do is re-design the objects so that the interaction between them is not circular. However, that is kind of necessary here!
Do I have to write a "getter" for each piece of data that might be shared between the objects? That seems like a lot of coding (I have a lot of data which might be shared). However, that also seems rather complicated because the "getter" (specific to a piece of data) would also have to satisfy an abstract interface.
In higher level languages like Python, this would be easy as we could simply create a new data type as a composite of the models but that's not possible, as far as I am aware, in Fortran.
Thanks in advance. Any help is greatly appreciated.
Edit: Following the discussion with francescalus below, select type
is an option. Indeed, in the simple example given above, select type
would be a good choice. However, in my actual code, this would result in large, nested select type
s and so if there is a way to do it without using select type
I would prefer it. Thanks to francescalus for pointing out my error regarding select type
.
To answer your two questions:
Is there a better way to design?
I don't quite know why you have so many contraints in your design, but in short yes. You can use a context manager for your models. I recommend you check out this answer: Context class pattern
Do you have to write a getter method on every model?
Not quite, if you use a context strategy on this specific issue, the only thing you need to implement on every model is a setter method that will share the data between models.
I implemented a working solution for this scenario in Python. Code speaks louder than words. I've avoided using any special features of Python to provide you a clear understanding of how to use a context in this case.
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()
Output
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
Explanation
In short, we create a context class which has a dictionary that can be shared across the different models. This implementation is very specific to the primitive data types you provided (I.E. x, y, z). If you need to compute the data before it is shared across the models you can still use this pattern by replacing the return of solveMe()
with a deferred promise.
FWIW, below is a similar attempt to access fields of another objects based on key/values (*). For simplicity, the main program gets one integer from a child1_t
object and sets a complex value to a child2_t
object (both of which are extended types of 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
Compile & results:
$ 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)
Though the above code deals with "data transfer" for only integer and complex, other kind of data may be added similaly in the select case
construct (without adding new getter/setter methods for each). Further, if necessary, we could overload get()
and set()
for different types of value
(e.g. set_int()
and set_real()
) via generic
keyword (in the parent_t
type), and override them in the extended types. The same also holds for value
of array type, maybe...
If the transfer (copy) of data between extended types are expensive (e.g., large arrays), I guess the getter could return pointers to them so that childX_t
's can communicate in a tightly coupled way (without knowing their implementation).
(But I guess there may be a simpler approach than doing a thing like the above, including Point 1 in the question (e.g., re-design of the program itself). Also, if we have some dictionary type in hand, the use of dictionaries (as in the other answer) seems more appealing to me.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With