Fortran: Enumeration, part 1
Matthias Noback
In the post about decoration we declared log levels as follows:
module logging_base
! ...
integer, parameter, public :: LOG_DEBUG = 0
integer, parameter, public :: LOG_INFO = 1
integer, parameter, public :: LOG_WARN = 2
integer, parameter, public :: LOG_ERROR = 3
integer, parameter, public :: LOG_FATAL = 4
! ...
end module logging_base
This is somewhat useful, but requires a dedicated only
mention when importing each of these constants, which isn’t very nice:
program main
use logging_base, only: log, LOG_DEBUG, LOG_WARN
! ...
call log('A debug message', LOG_DEBUG)
! ...
call log('A warning', LOG_WARN)
end program main
Although we could live with that, the bigger issue remains: this is not a type-safe solution. Any integer
may be passed as an argument for level
, and the compiler wouldn’t warn us. For example:
program main
! ...
integer :: any_integer
integer, parameter :: LOG_SPECIAL = 999
any_integer = 1234
! ...
call logger%log('An unknown kind of message', any_integer)
call logger%log('A special message', LOG_SPECIAL)
! or just
call logger%log('Any other integer', 10)
end program main
This is a common situation: we want to enforce a value to be one of a limited list of possible values. Other languages may offer an enum
or any other kind of enumeration type. But in Fortran we don’t have that. Or so I thought. In “Modern Fortran, Explained” I found a section on enumerations in the chapter “Interoperability with C”. It turns out that to support integrating Fortran applications/libraries with ones written in C (or the other way around), a special enum
type is available that maps to a C enum
. Declaring an enum
requires the import of the intrinsic module iso_c_binding
. Then we can define the same log levels we used before as an enum
(note the bind(c)
):
module logging_base
use iso_c_binding
implicit none(type, external)
private
public :: LOG_DEBUG
public :: LOG_INFO
public :: LOG_WARN
public :: LOG_ERROR
public :: LOG_FATAL
enum, bind(c)
enumerator :: LOG_DEBUG = 0
enumerator :: LOG_INFO = 1
enumerator :: LOG_WARN = 2
enumerator :: LOG_ERROR = 3
enumerator :: LOG_FATAL = 4
end enum
end module logging_base
Unfortunately, from a Fortran standpoint this enum
type is - sorry to say it - useless because:
- You can’t force a function argument or variable to be one of the enumeration values.
- You can’t give a name to the
enum
type, meaning you can’t access the values by their name as a component of the enum, likelog_level.LOG_WARN
.
The short section on enumerations in “Modern Fortran, Explained” mentions the use of integer(kind(...))
to declare an integer that is one of the enumerators, e.g.
integer(kind(LOG_WARN)) :: warn
I haven’t found a way to make use of such a variable. Here’s how it behaves in a subroutine to which I’m passing LOG_WARN
as an argument:
subroutine check_log_level(level)
integer, intent(in) :: level
integer(kind(LOG_WARN)) :: warn
! Output: warn=24574
print *, 'warn=', warn
! Output: kind(warn)=4 (the default integer kind)
print *, 'kind(warn)=', kind(warn)
! Output: kind(level)=4 (the default integer kind)
print *, 'kind(level)=', kind(level)
! Output: LOG_WARN=2 (the value we assigned to it)
print *, 'LOG_WARN=', LOG_WARN
if (level == LOG_WARN) then
! It should be no surprise, that this is indeed the case
print *, 'Log level is LOG_WARN'
end if
end subroutine check_log_level
Maybe we’ll have better support in a new Fortran standard. For now, we have to extend the type system ourselves, and of course that involves creating a derived type. This will give us type safety, and there are some additional benefits.
A derived type for log levels
Could we, at the very least, group the log level parameters in a derived type? That would allow us to give a name to the enumeration (log_level_t
), something we can’t do if we just have a list of constants. It also allows us to remove the prefix LOG_
:
! Doesn't work...
type :: log_level_t
integer, parameter :: DEBUG = 0
integer, parameter :: INFO = 1
integer, parameter :: WARN = 2
integer, parameter :: ERROR = 3
integer, parameter :: FATAL = 4
end type log_level_t
Unfortunately, this isn’t possible: you can’t use parameter
on data components. A derived type can’t have static components, like a class in other languages, which can have constants. You always need an instance of the type before you can do something with it.
If we remove parameter
, the code will compile:
type :: log_level_t
- integer, parameter :: DEBUG = 0
+ integer :: DEBUG = 0
- integer, parameter :: INFO = 1
+ integer :: INFO = 1
- integer, parameter :: WARN = 2
+ integer :: WARN = 2
- integer, parameter :: ERROR = 3
+ integer :: ERROR = 3
- integer, parameter :: FATAL = 4
+ integer :: FATAL = 4
end type log_level_t
Before you can use a specific log level you’d have to instantiate the whole type. The simplest way is to do it implicitly, with a pre-allocated local variable:
use logging_base, only: log, log_level_t
type(log_level_t) :: log_level
! ...
call log('Message', log_level%WARN)
This is starting to look better; we have only one import for any number of log levels we need. However, we don’t want to have the local variable everywhere we log, it’s just a hassle. Maybe we can return a log_level_t
instance from a function?
function log_level() result(res)
type(log_level_t) :: res
end function log_level
Unfortunately, this doesn’t save us. When trying to use the function to directly access one the log levels:
call log('Message', log_level()%WARN)
We get a compiler error:
| print *, log_level()%WARN
| 1
Error: The leftmost part-ref in a data-ref cannot be a
function reference at (1)
It turns out, we always have to put the result of log_level()
in a variable first, before we can use it. There’s another option though, which is to ensure the variable already exists and can be imported directly. We can do this by making it a public module variable of logging_base
. We remove the function we created before and declare a variable with that name instead:
module logging_base
! ...
private
! ...
+ public :: log_level
type :: log_level_t
integer :: DEBUG = 0
integer :: INFO = 1
integer :: WARN = 2
integer :: ERROR = 3
integer :: FATAL = 4
end type log_level_t
+ type(log_level_t) :: log_level
! ...
end module logging_base
Now we no longer need to instantiate a local log_level_t
variable, but we can just import it and use any log level we want:
-use logging_base, only: log, log_level_t
+use logging_base, only: log, log_level
-type(log_level_t) :: log_level
! ...
call log('Message', log_level%WARN)
Making the constants really constant
Since the log levels are public data components of log_level
, users could modify them:
use logging_base, only: log, log_level
! Should not be possible:
log_level%WARN = 999
Of course, it’s better for an enumeration if its values remain constant. Anyway, it’s not an intended use case for log levels to be modified anyway.
To turn them into immutable values, we should first make the data components private, then add type-bound procedures for retrieving the respective integer
values. We don’t offer “setters”, so effectively the data components are constants now:
module logging_base
! ...
type :: log_level_t
private
integer :: debug_value = 0
integer :: info_value = 1
integer :: warning_value = 2
integer :: error_value = 3
integer :: fatal_value = 4
contains
procedure :: debug => log_level_debug
procedure :: info => log_level_info
procedure :: warning => log_level_warning
procedure :: error => log_level_error
procedure :: fatal => log_level_fatal
end type log_level_t
contains
function log_level_debug(self) result(res)
class(log_level_t), intent(in) :: self
integer :: res
res = self%debug_value
end function log_level_debug
! And so on...
end module logging_base
Note: we go from uppercase constants to regular snake-case data components and type-bound procedures now, as is the convention for naming these things. I also got rid of the totally unnecessary abbreviation of warning to “warn”.
Users of log_level_t
should now use the type-bound procedures:
use logging_base, only: log, log_level
-call log('A message', log_level%WARN)
+call log('A message', log_level%warning())
Collapsing to a single data component
When making data components private
, it becomes a lot easier to notice which procedures use them, and even if they are still needed.
In this case we notice that each data component is only used by one of the procedures, and are never modified. So we might as well return the hard-coded integers directly from those procedures:
module logging_base
! ...
type :: log_level_t
private
- integer :: debug_value = 0
- integer :: info_value = 1
- integer :: warning_value = 2
- integer :: error_value = 3
- integer :: fatal_value = 4
contains
procedure :: debug => log_level_debug
procedure :: info => log_level_info
procedure :: warning => log_level_warning
procedure :: error => log_level_error
procedure :: fatal => log_level_fatal
end type log_level_t
contains
function log_level_debug(self) result(res)
class(log_level_t), intent(in) :: self
integer :: res
- res = self%debug_value
+ res = 1
end function log_level_debug
end module logging_base
We have fixed some problems, but the most important one remains: you can still pass any integer
to log()
and the compiler won’t complain. We’ll fix that in part 2.