Dynamic String Generation and C++-style Output in Fortran
Marcus Mohr

=1



codelet

0.85





#1
Comment: #1















0000-0003-2942-8484
Geophysics, Department of Earth and Environmental
    Sciences, Ludwig-Maximilians-Universität München
  Munich
  Germany
marcus.mohr@lmu.de


§ ABSTRACT 
Using standard components of modern Fortran we present a technique to
dynamically generate strings with as little coding overhead as possible
on the application side. Additionally we demonstrate how this can be extended
to allow for output generation with a C++ stream-like look and feel.






<ccs2012>
   <concept>
       <concept_id>10011007.10011006.10011008.10011024.10011025</concept_id>
       <concept_desc>Software and its engineering Polymorphism</concept_desc>
       <concept_significance>500</concept_significance>
       </concept>


[500]Software and its engineering Polymorphism

Dynamic String Generation and C++-style Output in Fortran
    Marcus Mohr
    ^1 Université Paris-Saclay, CEA, Service de Thermo-hydraulique

et de Mécanique des Fluides, 91191 Gif-sur-Yvette, France

^2 Laboratoire de Physique de la Matière Condensée, CNRS,
École Polytechnique, 
Institut Polytechnique de Paris, 91120 Palaiseau, France
=======================================================================================================================================================================================================================================================================



acmauthoryear




§ MOTIVATION




Scientific simulation codes do not only perform large-scale I/O for reading
input datasets and storing final simulation results. Especially in the case
of long-running simulations, they also typically generate log messages to
inform the user on various details of the simulation run, ranging from
echoing steering parameters, over current time-step values, up to the progress
of iterative solvers and many more. These get send to either the
screen/terminal or a dedicated logfile or both.

In larger projects one quickly reaches a point where aspects such as the
following become highly desirable




  * employ a 'nice' and uniform formatting for log messages

  * allow users/developpers to select different levels of verbosity
  (e.g. debug, info, warning)

  * have different levels of indentation or another way to signal which
  program component logged a message 

  * allow switching the message destination (terminal, file or both)

  * …


Enforcing formatting rules and checking e.g. the current verbosity level
becomes cumbersome, if the corresponding write statements are cluttered
throughout the code. Obviously the standard idea to reduce code duplication
also applies here, i.e. one delegates the actual I/O operations, including
aspects like indentation, verbosity checking, etc. to a designated part of
the code, let's say a log_manager module.

An additional difficulty arises in MPI-parallel applications. Typically only
one MPI process e.g. the one with rank 0, is intended to generate log messages.
This implies that any generation of a log message must be wrapped inside code
for checking the rank of the executing MPI process. Again this check could
conveniently be encapsulated in said log_manager module, thus,
uncluttering the rest of the code.

Naturally the delegation of these decisions and code parts to a separate module
comes at a price. However, performance-wise the extra costs resulting from
calls to a different subprogram in such an approach can be considered
uncritical as in a typical simulation code the time spent for generating log
messages is negligible compared to the actual computational work.

While our hypothetical log_manager module handles the actual I/O
operations and possibly also deals with formatting issues and the like,
the generation of the actual text of a log message necessarily must happen
in the respective code part of the caller. The resulting text string must then
be send to the log_manager module e.g. by invoking a
log_write() subroutine.

Generating a string literal to pass to a subroutine is of course
straightforward and not different in Fortran than in any other
programming language employed in scientific programming



[frame=single,numbers=none]
call log_write( "Starting assembly of FE matrix" )




However, often log messages will contain information that is only available at
run-time, such as the number of vertices in an input mesh, the norm of a
residual vector or the current iteration count of a loop. Thus, we need to be
able to dynamically generate messages. Of course, this is nothing one
could not accomplish using standard Fortran tools. We just need to perform an IO
operation on an “internal file”, i.e. an existing string. Assume that for
this purpose our log_manager module provides a string of constant
length



[frame=single,numbers=none]
character(len=max_message_length) :: msg




Then logging e.g. the norm of a residual vector stored in the variable
resnorm could be achieved by



[frame=single,numbers=none]
write( msg, "( 'resnorm = ', E12.4E3)" ) resnorm
call log_write( msg )





Or we could output the entry  a_2,3 of a matrix via



[frame=single,numbers=none]
write( msg, "('(',I0,',',I0,') = ',F0.3)" ) 2, 3, a(2,3)
call log_write( msg )




giving us an output of e.g. ), optional, intent(in) :: spec
  character(len=:), allocatable          :: str

  character(len=1024) :: fmt

  ! if conversion specifier present: use it
  if ( present(spec) ) then
     write( fmt,"(3A)") "(", spec, ")"
     write( buffer, fmt ) val
  else
     fmt = "(I0)"
  end if

  write( buffer, fmt ) val
  str = trim( buffer )

end function io_int2str


Example of stringification function for 32-bit integers.




The second example is different in two respects. This time the argument to
v2s() is assumed to be of type real, with kind real64,
and we provide our own conversion specification as a second argument. The
latter is not required, as the approach allows us to define, as part of the
stringify module, (project-wide) rules for the format conversion. An
example can be found in the demonstrator code, <cit.>.

On the other hand, providing a conversion specification also e.g. allows us
to change the way logicals get converted. Our example io_bool2str()
implementation provides the options default (classic Fortran-style, 
i.e. T/F),
word (true/false), code (.true./.false.) and "switch" (on/off).
Thus, the third example passes ), intent(in)  :: str_in
  real(real64)    , intent(in)  :: val

  str_out = str_in // v2s( val )

end function real2stream






§ EXTENSIONS






 §.§ Multi-Line Messages



Our first extension will allow generation of mult-line messages. Classically
one would handle this in an output statement in Fortran by inserting an
end-of-record specifier '/' into the conversion format. So



[frame=single,numbers=none]
print "(A,/,A)", "1st line of message ...", "... and 2nd line"




will produce



1st line of message ...
... and 2nd line



A simple alternative is to directly insert the line-break into the string.
For this purpose the stringify module provides a string literal
newline that is a shorthand for the ASCII line-feed control
character[This works for Unix/Linux and Mac OS. In the case of the
Windows OS we need to replace this by a carriage return followed by a
line-feed.].



[frame=single,numbers=none]
character(len=*), parameter :: newline = char(10) ! LF only




Hence, the following codelet



[frame=single,numbers=none]
print "(A)", newline// " We can" // newline // " use multi" //    
     "-line" // newline // " strings, too!"




will output



 We can
 use multi-line
 strings, too!


The same can also be done with tab-stops.




 §.§ I/O for Arrays



The approach is not restricted to scalar types, but can be
extended to any kind of derived type. We start by giving an example for 
rank-1 arrays of integers, before considering a user-defined type.

We implement conversion of a rank-1 integer array in the function
io_intvec2str() given in Fig. <ref>. Assume that
the variable int_vec has the following four entries (1,7,-3,5),
then the codelet



[frame=single,numbers=none]
print "(A)", "An integer vector:" // newline // v2s(int_vec, "I2")




will print










 §.§ Example with a User-Defined Structure Type



We now demonstrate the extension to user-defined derived types. As an example
let use consider a simple type point3d_t for representing a point in
3D.

The first step is to add a type-bound procedure stringify_point
(lines 3-4)



[frame=single,numbers=left]
type :: point3d_t
   real(real64) :: x, y, z
 contains
   procedure, pass (this) :: stringify_point
end type point3d_t




The procedure then might be implemented as follows



[frame=single,numbers=left]
function stringify_point( this ) result( str_out )
  class(point3d_t), intent(in)  :: this
  character(len=:), allocatable :: str_out

  ! allocate deferred length string for using it in write()
  integer :: str_out_len = 6 + 3*4
  allocate( character(len=str_out_len) :: str_out )

  ! pretty-print point coordinates as triple
  write( str_out, "(3(A,SP,F3.1),A)" ) "(", this
     this
end function stringify_point




As second step we add a corresponding function to our stringify module
whose only task is to delegate string generation to the type-bound procedure.



[frame=single,numbers=left]
function io_point3d2str( val ) result(str)
  class(point3D_t), intent(in)  :: val
  character(len=:), allocatable :: str
  str = val
end function io_point3d2str




This new function io_point3d2str() is then added as another possiblity
to the overload list for v2s(). In order to simply place objects of
type point3d_t in the output stream, we extend as third and
final step the streamstyle module. For this we add a function
point2stream(), which, again, is syntactically identical to
real2stream() above, and add it to the list of overloads for the
concatenation operator.



[frame=single,numbers=left]
interface operator(//)
   module procedure int2stream, real2stream, point2stream
end interface operator(//)




Now the following codelet



[frame=single,numbers=left]
type(point3d_t) :: point = point3d_t( 0.5, 1.0, -2.0 )
write(*,'(A)') "Point at coords = " // point // " in domain"




will output , …) and nested I/O operations (first allowed in F2023). As
these
are well-implemented, see e.g. <cit.>, there should be no
portability issues. The demonstrator at <cit.> has successfully
been test with the following compilers:






2cCompiler     version 


AMD/AOCC          flang         12.0.0    

GNU               gfortran      12.2.0    

Intel/classic     ifort         2021.11.1 

Intel/OneAPI      ifx           2024.0.2  

Nvidia            nvfortran     21.9-0    







ACM-Reference-Format








