Case Study 2: Designing a Forward-Compatible Config Module for the Solver
"The best way to predict the future is to invent it." — Alan Kay
Executive Summary
You want your heat solver to adopt Fortran 2023 — enumeration types for its configuration, conditional
expressions in its logic — but you also have to ship code that builds on the compilers your collaborators
have installed today, several of which do not support those features yet. These goals seem opposed. They are
not, if you design for the migration in advance: put every choice that a 2023 feature will one day improve
behind a small, single-purpose interface, so that flipping the feature on later is a local edit, not a
scavenger hunt. In this study you design a solver_config module that is clean and portable now and
deliberately shaped so that enumeration types and conditional expressions drop in with minimal churn. Then
you build it with fpm and run it in the browser Playground — proving the new tooling as you prepare for the
new language.
Skills applied
- Designing an API around choice points so future features (enumeration types, conditional expressions) localize to one place (§39.1, §39.4).
- Separating shipped from coming, in code structure, not just in comments (§39.4 honesty).
- Packaging with fpm and running in the Playground / LFortran (§39.3; Chapter 16).
- Derived types with sensible defaults and
puremapping functions (Chapters 9 and 6).
Background
The solver's run is governed by a handful of choices: which time-stepping scheme, which boundary condition,
what timestep. Today you encode each as an integer flag. Tomorrow you would like each to be a distinct,
compiler-checked enumeration type, so that cfg%scheme = bc_neumann — assigning a boundary value to a
scheme field — is a compile error rather than a silent 2 that runs the wrong integrator.
The design question is not "which is better" (the enumeration type, clearly) but "how do I build the integer-flag version so that the enumeration-type version is a small, safe change later?" That is a software-engineering question, and answering it well is what makes this a design case study rather than a syntax tour.
Phase 1: The Config Type, With Choice Points Named
Start from named constants and a derived type with defaults. The constants are grouped and commented with their future type name, so the migration target is written down in the code:
module solver_config
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: config_t, scheme_euler, scheme_rk4, bc_dirichlet, bc_neumann
public :: scheme_name, bc_name, default_config
! Future: enumeration type `scheme_t`. Named constants until compilers support it.
integer, parameter :: scheme_euler = 1
integer, parameter :: scheme_rk4 = 2
! Future: enumeration type `bc_t`.
integer, parameter :: bc_dirichlet = 1
integer, parameter :: bc_neumann = 2
type :: config_t
integer :: scheme = scheme_euler ! future: type(scheme_t)
integer :: bc = bc_dirichlet ! future: type(bc_t)
real(dp) :: dt = 0.01_dp
end type config_t
The private/public discipline from Chapter 8
matters here: callers see the constants and the type, not the raw integer 1/2, so when those constants
become enumerators of a real type, the callers barely change.
Phase 2: One Place to Migrate
Every point where the integer flag is interpreted — turned into a name, a branch, a dispatch — is a place a
2023 feature will touch. Concentrate them. Here two pure mapping functions are the only code that reads
the flags; everything else passes them around opaquely:
contains
pure function scheme_name(s) result(name)
integer, intent(in) :: s
character(len=:), allocatable :: name
select case (s)
case (scheme_euler)
name = 'euler'
case (scheme_rk4)
name = 'rk4'
case default
name = 'unknown' ! future enumeration type makes this unreachable
end select
end function scheme_name
pure function bc_name(b) result(name)
integer, intent(in) :: b
character(len=:), allocatable :: name
select case (b)
case (bc_dirichlet)
name = 'Dirichlet'
case (bc_neumann)
name = 'Neumann'
case default
name = 'unknown'
end select
end function bc_name
pure function default_config() result(cfg)
type(config_t) :: cfg
! all defaults come from the type definition; override at the call site
end function default_config
end module solver_config
Note the payoff already visible in the comment: once scheme is a real enumeration type, the compiler
guarantees s is one of the enumerators, and the case default becomes provably unreachable — the "unknown"
path exists today only because a bare integer could be anything. The design makes the future improvement's
benefit legible now.
Phase 3: Build It With fpm, Run It in the Browser
Package the module the fpm way you learned in
Chapter 16: library
code in src/, the program in app/, a test in test/.
solver-config/
├── fpm.toml
├── src/
│ └── solver_config.f90
├── app/
│ └── main.f90 program show_config (the driver below)
└── test/
└── test_config.f90 checks scheme_name(scheme_rk4) == 'rk4'
name = "solver-config"
[build]
auto-executables = true
The driver, portable under -std=f2018:
program show_config
use solver_config
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
type(config_t) :: cfg
cfg = default_config()
cfg%scheme = scheme_rk4 ! reads as a name, not a magic 2
cfg%bc = bc_neumann
print '(a, a)', 'scheme = ', scheme_name(cfg%scheme)
print '(a, a)', 'bc = ', bc_name(cfg%bc)
print '(a, f5.2)', 'dt = ', cfg%dt
end program show_config
$ fpm run
scheme = rk4
bc = Neumann
dt = 0.01
Sanity check, by hand. default_config() sets dt = 0.01; we override scheme to scheme_rk4 (the
constant 2), which scheme_name maps to 'rk4', and bc to bc_neumann, which bc_name maps to
'Neumann'. dt prints under f5.2 as 0.01. Outputs match.
Now prove the tooling: paste the module and driver into the fortran-lang Playground in your browser and press run — no fpm, no local compiler — or load them into LFortran and evaluate the driver interactively. You have just run your solver's configuration layer through two tools that did not exist for Fortran a few years ago, which is the ecosystem half of "the language is alive."
⚠️ Keep it honest. The
fpm.tomlkeys and any Playground/LFortran specifics evolve quickly; confirm them against the current fortran-lang sites rather than trusting a snapshot. The design — choice points behind small interfaces — is what will not go stale.
Phase 4: The Readiness Checklist
Because you designed for migration, "turn on Fortran 2023" becomes a short, auditable checklist rather than a rewrite. Write it into the project so the next maintainer knows the plan:
| When your compiler supports… | Change, localized to… | Fallback kept? |
|---|---|---|
| Enumeration types | the constant groups + the two field declarations in config_t |
yes — named constants stay until every target compiler is ready |
| Conditional expressions | any if/else inside a single expression (none critical here yet) |
yes — the if form still compiles |
| Degree trig | the solver's geometry module (not this config) | yes — the pi/180 helper |
Each row touches one place, because Phase 1 and Phase 2 put it there. That is the entire return on the design work: the cost of adopting the future was paid down in advance, in structure.
Discussion Questions
- We made
scheme_name/bc_namethe only readers of the flags. What kinds of bugs does concentrating interpretation like this prevent, beyond the migration convenience? - The
case default → 'unknown'branch is "dead" once enumeration types arrive. Is it wasteful to write it now? Argue both sides. (Consider: what does a bare integer field permit today?) - Why keep the named-constant fallback after your own compiler supports enumeration types? (Whose compilers must you also build on?) Tie this to the reproducibility discipline of Chapter 37.
- Where would a conditional expression naturally appear once the solver's stepping logic is added to this config layer, and how would you keep that line portable in the meantime?
Your Turn: Extensions
- Option A (build). Complete the fpm project: write
test/test_config.f90that fails (error stop) ifscheme_name(scheme_rk4) /= 'rk4', and runfpm test. You now have a regression test guarding the mapping. - Option B (design). Add a third choice — the output format (
text,vtk) — as a new named-constant group and mapping function, following the same pattern. Notice how mechanical it is: that repeatability is the sign of a good design. - Option C (forecast). Rewrite the
config_tfield declarations as you believe Fortran 2023 enumeration types would spell them, clearly commented as unverified, then check your guess against your compiler's documentation. Where were you right, and where did the real syntax surprise you?
Key Takeaways
- Design for the migration, not just the feature. Put every choice a future feature will improve behind a small interface; adopting the feature then becomes a local, low-risk edit.
- Structure encodes honesty. "Shipped vs coming" is not only a comment — it is named constants now with a documented enumeration-type target, and one place to change when the time comes.
- Prove the tooling as you go. fpm builds it; the Playground and LFortran run it with nothing installed. The ecosystem is part of the language's aliveness, and it is usable today.
- The future is cheaper if you invent it deliberately. A little design now buys a painless upgrade later — which is exactly the bet the standards committee makes for the whole language.