A couple of (probably trivial) OOP questions

  1. Why do you have to use the CLASS keyword for the passed object argument in type bound procedures if you arent doing any polymorphism etc. Maybe this has changed (has it). I’ve always thought the CLASS keyword was a mistake. My preference would have been a POLYMORPHIC attribute (ie Type(a_t), POLYMORPHIC) to signal the derived type can be polymorphic. This would have kept the OOP more “Fortranic”

  2. I had the following situation with abstract classes in a code.

    a.) I defined an abstract class for a solver class (solver_t) that contains one deferred method (a solveField method)

    b.) I extend that class to create separate fluid solver and stuctures solver classes (lets call them fluidSolver_t and solidSolver_t). Both have their own specific implementation of the solveField method

c.) I created a “Factory” to define which solver I wanted to use and passed that back to the main program where its called using something like the following code. Which solver is selected is based on an input value and parameters set by enumerations

 class(solve_t), allocatable :: solver ! solve_t is abstract
  select case(mySolver)
     Case(fluidSolver)
         allocate(fluidsolver_t :: solver)
     Case(solidSolver)
         allocate(solidsolver_t :: solver)
  end select

When I tried to expand the above Case clauses to add a call to an init function defined in the child classes (but not in the abstract class)
ie.
Case(fluidSolver)
allocate(fluidsolver_t ::solver)
call solver%init()

etc. ifort complained about the init procedure was not in the solver_t class. I had to wrap the call to init in a select type before it works. ie
allocate(fluidsolver_t :: solver)
select type(solver)
type is (fluidsolver_t)
call solver%init()
end select

my question is why in this case is the select type needed. Shouldn’t the allocate statement create an instance of a fluidsolver_t or solidsolver_t object. It apparently knows that solver is now a fluidsolver_t or a solidsolver_t or the select type wouldn’t work. This is one of the things I find most frustrating about the Fortran OOP. How things are implemented (or more precisely how you are forced to implement something) just doesn’t make sense. I finally gave up on the OOP approach and got rid of the abstract class and made fluidsolver_t and solidsolver_t standalone classes and wrote a separate routine to explicitly call each classes solveField method based on user input.

This is a prime example of why I’ve tried to avoid using the Fortran OOP. Not because I’m opposed to OOP, just how its implemented in Fortran.

  1. See proposal link below for some background on this and a possible solution into the future. At present, a derived type in Fortran that is not a SEQUENCE or a BIND(C) type is extensible - no other exceptions - and the established semantics toward type extension requires the passed object argument to be a polymorphic type. Fortran syntax, like it not, adopted the CLASS word to imply polymorphism. So that’s just how things are.
    Feature proposal: open to derive an inextensible derived type · Issue #37 · j3-fortran/fortran_proposals · GitHub

  2. If your design calls for it, you can introduce a DEFERRED or direct binding to your abstract type named Init and you can then reference it as call solver%init()

@rwmsu, as discussed in the other thread, object-oriented paradigm generally, independent of the language, requires some study first following by good analysis and design before the programming. Then there are always language-specific intricacies: C++, Java, .NET, OCaml, etc. all have their quirks and differences. Fortran comes with its own peculiarities. It requires some effort to gain some facility with OO in Fortran and all the other languages, that’s just how it is with the paradigm.

However it is quite workable once you put in that effort. Moreover Fortran and the tooling can be greatly improved for efficient practice, the above proposal is but one low-hanging fruit.

@FortranFan, I thought about adding an init to the abstract type but that forces me to use the same interface for init for both fluids and solids. I need the flexibility for the interfaces to be different. Yes I could wrap the class specific init programs with a generic init with the same interface but to me that is just forcing you to write a wrapper routine that does nothing but call the routine you wanted to use in the first place. However, my second question still stands. After the allocate why is solver still treated as though its the abstract type and not the child class used to create the solver instance.

Because the language semantics have been established to work off of the declared type which in your example here is the abstract type solver_t. Other languages offer shorthand notation toward static casting to help the practitioners in such situations, Fortran instead has the verbose SELECT TYPE construct.

This becomes another important analysis and design consideration i.e., how to avoid / minimize the need for such casting during the consumption of one’s OO code.

You might consider this pattern that avoids the select type without needing to put the %init method into the abstract class. It’s no shorter, but some might find it aesthetically less obnoxious.

class(solver_t), allocatable :: solver
select case (mySolver)
case (fluidSolver)
  block
    type(fluidsolver_t), allocatable :: tmp
    allocate(tmp)
    call tmp%init(...)
    call move_alloc(tmp, solver)
  end block
case(solidSolver)
  ... and so on for other cases
end select
1 Like

Instead of an %init routine you could also use a structure constructor, or an overloaded version, along with allocation upon assignment:

program test_polymorphism

  implicit none
  
  type, abstract :: solver_t
  end type
  type, extends(solver_t) :: fluid_solver_t
    real :: a
  end type
  type, extends(solver_t) :: solid_solver_t
    integer :: a
  end type

  class(solver_t), allocatable :: s
  integer :: senum
  character(len=32) :: sstr

  if (command_argument_count() == 0) then
    print *, "Usage:  test_polymorphism <0|1>"
    stop 1, quiet=.true.
  end if

  call get_command_argument(1,value=sstr)
  read(sstr,*) senum

  select case(senum)
  case(0)
    s = fluid_solver_t(a=42.0)
  case(1)
    s = solid_solver_t(a=21)
  end select

  ! print dynamic type
  select type(s)
  type is (fluid_solver_t)
    print *, "0 - fluid_solver_t: a = ", s%a
  type is (solid_solver_t)
    print *, "1 - solid_solver_t: a = ", s%a
  end select

end program
$ gfortran -Wall ./test_polymorphism.f90 -o test_polymorphism
$ ./test_polymorphism 0
 0 - fluid_solver_t: a =    42.0000000    
$ ./test_polymorphism 1
 1 - solid_solver_t: a =           21

I think this is roughly the idea that @rouson has shared in the other OOP thread:

In my latest work, I almost never have setters because I try to construct whole objects and not leave the object in a partially defined state.

and also in the past, e.g. in his FortranCon 2021 talk:

Construct whole objects with generic interfaces serving as user-defined structure constructors → no setters.

1 Like

Thanks to all for your suggestions. I finally went with @FortranFan’s suggestion which I had considered originally but rejected because I thought it would lead to redundant code and embedded an init method in the abstract type. This forced me to move some of the shared pieces of the initialization process for both the fluid and structures objects into new wrapper routines but at the end of the day that (in retrospect) was the path of least resistance, at least to avoid a select type. On a side note, this whole exercise was triggered by an attempt to get my code to compile with the NVIDIA compiler without generating an ICE. All my initial attempts to figure out what was happening pointed to the Factory routine I was using to construct the solver objects. It appeared to be dying on a USE statement that brought in the abstract solver type. Turns out that wasn’t the problem. Just prior to where the compiler said the error occured, I was bringing in another module that contained an implied size array definition of an array of character strings. Something like

character(len=5), dimension(*), parameter :: plotnames = ["a ", "b ", …]

The ICE disappeared when I set the actual dimension to a fixed value

character(len=5), dimension(30), parameter etc.

I would report this to NVIDIA but the last bug I reported to them 4 or 5 years ago still to the best of my knowledge hasnt’ been fixed. Sadly, NVIDIA appears to be in no hurry to bring their compiler up to Fortran 2008 (much less Fortran 2018). I wanted to get my code working with nvfortran so I could try my hand at GPU programming and justify what I just spent to buy a high end NVIDIA card.

Sad.

Re: what you “just spent to buy a high end NVIDIA card”, you likely with need to restrict yourself to CUDA FORTRAN which, to my eyes, is inelegant and unpleasant programming.

I personally find GPU programming as still highly nascent, I would view any spending on it as personal “R&D” write-off, that there is far too much development and learning yet to be completed when it comes to standard Fortran (e.g., DO CONCURRENT) and also standard C++, and for now, I would stick any of my spending (time and money) to Intel GPUs and Intel oneAPI as a testbed for such efforts into the foreseeable future. NVIDIA is entirely hopeless when it comes to Fortran.

@FortranFan, I actually agree to a certain point about the current status of GPU programming. Even though OpenMP and OpenACC give you a path other than CUDA FORTRAN, its still too tied to individual hardware vendors. AMDs cards appear to be a little friendlier to floating-point calculations (less of a hit if you want to use FP64) but I’m not sure if the support exists for programming them that exists for the Nvidia cards. Also, even though Intel is now a player in the GPU world, its inital offerings don’t match up in floating point performance (for engineering applications - not sure about graphics) with Nvidia or AMD. The card I bought (RTX A4500) can do a theoretical 23 TFlops in FP32 and around 700 Gflops FP64 (if you can actually get it to run in FP64). Like it or not GPUs and similar devices have become an important tool in scientific computing. Unfortunately, as you suggest the ability write codes (in any language) that can use them effectively has not kept up with the shear number crunching capability of the hardware. Also, people have been using GPUs for number crunching for over 10 years now so I wouldn’t call it “nacent”. Again as you suggest though, its still too highly tied to a particular vendor.

1 Like