Lessons learnt from polymorphism and dependency injection

@lewisjarednz FYI. I didn't know where else to put this so I put it here.

The relevant branch is https://gitlab.com/magicc/fgen/-/commits/timesteps-zn

I wanted to tweak things to introduce a new, intermediate, recorder object to deal with the translation from the solver to the results (not all solver steps go in results, but there are different rules about how to do this). In order to do that, I wanted to use polymorphism so the solve_ivp routine just received a Recorder and didn't need to know exactly how it functioned.

In short, it turns out that doing this sort of pattern in Fortran is fiddly. The actual setup isn't too bad, you define the base class in one module, then the child classes in their own modules. The one issue here is that you use inheritance, which I find in general to be a less nice pattern than using a protocol in Python because it is less clear where all the attributes and methods are defined. However, if we only stick to one level of inheritance, that is less of an issue because you have relatively few places to look.

The constructor pattern is super nice and works very well I would say. Props to @lewisjarednz for working that one out in MAGICC.

The really painful bits are dealing with pointers and allocations to handle this polymorphism. This is obviously just a consequence of Fortran being low-level language, but it is brain-bending if you're not used to it (e.g. you come from the Python world like me). I also suspect it isn't super easy to test (although I could be wrong about this).

In the function that created the classes, I found that I had to have a pointer to the base class (the line was class(BaseRecorder), pointer :: recorder) and then target instances of the two child types hanging around just in case they were used (I couldn't work out how to only allocate them if I needed them so I had these two lines type(RecorderSpecificTimes), target :: recorder_specific_times and type(RecorderAllSteps), target :: recorder_all_steps). The actual initialisation is not too painful after that though, something like

recorder_all_steps = RecorderAllSteps(max_step_count=a_max_step_count)
recorder => recorder_all_steps

From this point onwards you can then just use the recorder like any other old pointer.

I think it's not a bad pattern, but the indirection is quite high in Fortran even for a basic use case like this. As a result, a switch or if statement is actually probably no harder to understand if there's only 2-3 cases to handle. When there's more, the tradeoff probably swings the other way and it probably makes sense to use the polymorphism/dependency injection anyway.

Hope that helps. Probably going to be one of the trickier tradeoffs we have to repeatedly deal with in Fortran land.