Attempting type erasure in Fortran

Yesterday evening I watched a presentation by Klaus Iglberger on Breaking Dependencies: Type Erasure - A Design Analysis from CppCon 2021. In C++, type erasure can be implemented with a cumbersome template pattern using a base class that contains two private structure templates, and a public constructor function template. Once the pieces fall together you are quite astonished how simple the client code looks (but the template machinery behind it is scary…):

int main()
{
   using Shapes = std::vector<Shape>;

   // Creating some Shapes
   Shapes shapes;

   shapes.emplace_back( Circle{ 2.0 } ):
   shapes.emplace_back( Square{ 1.5 } ):
   shapes.emplace_back( Circle{ 4.2 } ):

   // Drawing all shapes
   drawAllShapes( shapes );
}

Anyways, this got me wondering, how close can we get in Fortran. The example in Klaus’ talk are geometric shapes. The goal is to draw/serialize the shape while preserving value semantics as far as possible. While over-simplified, you can imagine the shapes as a simplified X Window Server (e.g. you have to render windows and communicate their state with a remote system).

Following his talk, I implemented four different patterns, shown in the folded boxes below. All four are able to run on gfortran 11.1 and ifort 2021.5. In the process of writing the code I also found two or three ifort internal compiler errors.

Naive inheritance
!> shapes_naive_inheritance.f90

module shape_base_module

implicit none
private

public :: shape_base

type, abstract :: shape_base
contains
  procedure(draw_shape), deferred :: draw
  procedure(serialize_shape), deferred :: serialize
end type

abstract interface
  impure elemental subroutine draw_shape(shape)
    import shape_base
    class(shape_base), intent(in) :: shape
  end subroutine
  impure elemental subroutine serialize_shape(shape)
    import shape_base
    class(shape_base), intent(in) :: shape
  end subroutine
end interface

end module

module shape_circle_module

  use shape_base_module
  implicit none

  type, extends(shape_base) :: shape_circle
    real :: radius
  contains
    procedure :: draw => draw_circle
    procedure :: serialize => serialize_circle
  end type

  contains

  impure elemental subroutine draw_circle(shape)
    class(shape_circle), intent(in) :: shape 
    ! ... implementation ...
  end subroutine

  impure elemental subroutine serialize_circle(shape)
    class(shape_circle), intent(in) :: shape 
    print *, "CIRCLE    ", shape%radius
  end subroutine

end module

module shape_rectangle_module

  use shape_base_module
  implicit none

  type, extends(shape_base) :: shape_rectangle
    real :: width, heigth
  contains
    procedure :: draw => draw_rectangle
    procedure :: serialize => serialize_rectangle
  end type

contains

  impure elemental subroutine draw_rectangle(shape)
    class(shape_rectangle), intent(in) :: shape 
    ! ... implementation ...
  end subroutine

  impure elemental subroutine serialize_rectangle(shape)
    class(shape_rectangle), intent(in) :: shape 
    print *, "RECTANGLE ",shape%width, shape%heigth
  end subroutine

end module

!> Public-facing shape module
!>
!>   - Provide single entry point to the entire type hierarchy
!>   - Make names more manageable by renaming
!>
module shapes

  !> The abstract base type
    use shape_base_module, only: shape => shape_base

  !> The concrete child types
    use shape_circle_module, only: circle => shape_circle
    use shape_rectangle_module, only: rectangle => shape_rectangle

end module


program process_shapes

  use shapes
  implicit none

  write(*,*) "Process Circles"
  call process_circles()

  write(*,*) "Process Circles and Rectangles"
  call process_circles_and_rectangles()

contains

subroutine process_circles()
  class(shape), allocatable :: collage(:)
  integer :: i

  ! We can allocate an array of 3 circles
  allocate( circle :: collage(3))
  
  ! it could also be rectangles, e.g.
  ! allocate( rectangle :: collage(5))
  ! but it seems we can't mix both shapes, even though they
  ! are part of the same hierarchy...

  ! How do we initialize them?
  !
  ! a) by assignment? (gfortran doesn't allow this, ifort does!)
  !

  !collage(1) = circle(12.)
  !collage(2) = circle(13.)
  !collage(3) = circle(14.)
  !call collage%serialize()
  !print '()'

  !
  ! b) using select_type?
  !
  do i = 1, 3
    associate(item => collage(i))
    select type(item)
      type is (circle)
        item%radius = i*5
    end select
    end associate
  end do

  call collage%serialize()  ! But it's nice that the 
                            ! elemental attribute works!

end subroutine

subroutine process_circles_and_rectangles()

  !> If we want a list of mixed items, we need to encapsulate
  !> the polymorphic variable.

  type :: shape_item
    class(shape), allocatable :: item
  contains
  end type

  type(shape_item), allocatable :: items(:)
  integer :: i

  allocate(items(5))

  allocate(items(1)%item, source=circle(1.0))
  allocate(items(2)%item, source=circle(2.0))
  allocate(items(3)%item, source=circle(3.0))
  allocate(items(4)%item, source=rectangle(3.,4.))
  allocate(items(5)%item, source=rectangle(5.,7.))

                   !^^^ this look terrible 
  ! If we want to make this nicer, we need functions which will convert
  ! a shape to a shape_item. 

  ! Also the type-bound procedures are now one level deeper.
  ! We need to loop through them, or add the type bound procedures
  ! to our new shape_item that wraps the polymorphic class...

  do i = 1, size(items)
    call items(i)%item%serialize()  
  end do

  ! Let's go back to the drawing board, and try the 
  ! _strategy pattern_ instead

end subroutine

end program
Strategy pattern (via inheritance)
!> shapes_strategy.f90
!>
!> Shape (Strategy pattern using abstract base class)
!>
module shape_base_module

implicit none
private

public :: shape_base
public :: shape_base_set_strategies

! The inheritance hierarchy was getting a bit wild already, even with two shapes
! and two procedures.
!
! Let's try composition instead of inheritance. 
! We will keep the abstract base class, but this time it will
!
type, abstract :: shape_base
  private
  !
  ! The base class now contains two strategies; any child class
  ! will initialize these pointers to null, so statements such as
  ! allocate will not work correctly...
  !
  procedure(draw_strategy), pointer, pass(shape) :: draw_doit => null()
  procedure(serialize_strategy), pointer, pass(shape) :: serialize_doit => null()
contains
  ! The two concrete procedures only perform the 
  ! job of calling the strategies.
  procedure, non_overridable, public :: draw => draw_shape_base
  procedure, non_overridable, public :: serialize => serialize_shape_base
end type

abstract interface
  ! These can't be elemental, since they will be pointer targets
  subroutine draw_strategy(shape)
    import shape_base
    class(shape_base), intent(in) :: shape
  end subroutine
  subroutine serialize_strategy(shape)
    import shape_base
    class(shape_base), intent(in) :: shape
  end subroutine
end interface

contains

  ! But these are still elemental.
  impure elemental subroutine draw_shape_base(shape)
    class(shape_base), intent(in) :: shape
    call shape%draw_doit()
  end subroutine
  impure elemental subroutine serialize_shape_base(shape)
    class(shape_base), intent(in) :: shape
    call shape%serialize_doit()
  end subroutine

  subroutine shape_base_set_strategies(shape,draw,serialize)
    class(shape_base), intent(inout) :: shape
    procedure(draw_strategy) :: draw
    procedure(serialize_strategy) :: serialize

    shape%draw_doit => draw
    shape%serialize_doit => serialize
  end subroutine

end module

module shape_circle_module

  use shape_base_module, set_strategies => shape_base_set_strategies
  implicit none
  private

  public :: shape_circle

  type, extends(shape_base) :: shape_circle
    real :: radius
  end type

  interface shape_circle
    module procedure :: shape_circle_new
  end interface

  contains

  function shape_circle_new(radius) result(circle)
    real, intent(in) :: radius
    type(shape_circle) :: circle
    
    call set_strategies(circle, &
        draw_circle, &
        serialize_circle)

    circle%radius = radius
  
  end function

  subroutine draw_circle(shape)
    class(shape_base), intent(in) :: shape 
    ! ... implementation ...
    select type(shape)
      type is (shape_circle)
        print *, "Drawing circle ..."
    end select

  end subroutine

  subroutine serialize_circle(shape)
    class(shape_base), intent(in) :: shape

    ! Suddenly, we need to use select type here !!!

    select type(shape)
      type is (shape_circle)
      print *, "CIRCLE    ", shape%radius
    end select

  end subroutine

  ! In all procedures we will need to use select_type, because
  ! the procedure pointer in the base class expects an
  ! object of class(shape_base)... 

end module

module shape_rectangle_module

  use shape_base_module, set_strategies => shape_base_set_strategies
  implicit none
  private

  public :: shape_rectangle

  type, extends(shape_base) :: shape_rectangle
    real :: width, heigth
  end type

  interface shape_rectangle
    module procedure :: shape_rectangle_new
  end interface

  contains

  function shape_rectangle_new(width,heigth) result(rectangle)
    real, intent(in) :: width, heigth
    type(shape_rectangle) :: rectangle
    
    call set_strategies(rectangle, &
        draw_rectangle, &
        serialize_rectangle)

    rectangle%width = width
    rectangle%heigth = heigth
  
  end function

  subroutine draw_rectangle(shape)
    class(shape_base), intent(in) :: shape 
    ! ... implementation ...
    select type (shape)
    type is (shape_rectangle)
      print *, "Drawing rectangle ..."
    end select
  end subroutine

  subroutine serialize_rectangle(shape)
    class(shape_base), intent(in) :: shape
    select type (shape)
    type is (shape_rectangle)
      print *, "RECTANGLE ",shape%width, shape%heigth
    end select
  end subroutine

end module


module shapes

  !> The abstract base type
    use shape_base_module, only: shape => shape_base

  !> The concrete child types
    use shape_circle_module, only: circle => shape_circle
    use shape_rectangle_module, only: rectangle => shape_rectangle

    implicit none
    public

end module

program process_shapes

  use shapes
  implicit none

  write(*,*) "Process Circles"
  call process_circles()

contains

subroutine process_circles()

  class(shape), allocatable :: collage(:)
  integer :: i

  ! Allocation like this works, however the strategy pointers
  ! are still set to null...
  allocate( circle :: collage(3))
  
  ! it could also be rectangles, e.g.
  ! allocate( rectangle :: collage(5))
  ! but it seems we can't mix both shapes, even though they
  ! are part of the same hierarchy...

  ! How do we initialize them? We need to combine
  ! select type and our overloaded structure constructor,
  ! using the keyword argument to select the only one - weird...
  !
  do i = 1, 3
    select type (collage)
    type is (circle)
      collage(i) = circle(radius=5*i)
    end select
  end do

  call collage%serialize()  ! The elemental routines, still works, hooray!

end subroutine

! Overall, we don't seem to by doing any better than the
! hierarchy version. The client code needs select_type, we still can't
! mix different items of the same child types.

end program
Bridge pattern (Pimpl)
!> shapes_strategy_pimpl.f90

!> This is what a Fortran progammer who has never heard of 
!> inheritance, polymorphism, or pointers mights do.
!> Instead, we rely exclusively on compostion and allocatable arrays
!>
module shape_implementation
  
  implicit none
  private

  public :: shape_storage

  type :: shape_storage
    integer :: ishape = -1
    real, allocatable :: data(:)
  end type

  !> Different shapes are tightly coupled with the shape storage
  public :: circle_storage
  public :: rectangle_storage

  !> The "strategies", this time as static procedures. 
  public :: draw_program
  public :: serialize_program

  integer, parameter :: IEMPTY = -1
  integer, parameter :: ICIRCLE = 1
  integer, parameter :: IRECTANGLE = 2
  integer, parameter :: ITRIANGLE = 4
  ! ...

contains

  !> Constructor functions
  !> All share same data representation

  function circle_storage(radius) result(str)
    real, intent(in) :: radius
    type(shape_storage) :: str
    str = shape_storage( &
      ICIRCLE, &
      [radius] &
    )
  end function

  function rectangle_storage(width,heigth) result(str)
    real, intent(in) :: width, heigth
    type(shape_storage) :: str
    str = shape_storage( &
      IRECTANGLE, &
      [width, heigth] &
    )
  end function


  impure elemental subroutine draw_program(str)
    type(shape_storage), intent(in) :: str

    select case(str%ishape)
    case(ICIRCLE)
      print *, "Drawing circle ..."
    case(IRECTANGLE)
      print *, "Drawing rectangle ..."
    case default
      error stop "Shape not available..."
    end select
  end subroutine

  !> The implementations are placed in the same subprogram, because they 
  !> share the same storage. Potentially you could introduce a 
  !> sub-module to hide the implementation details and use more natural
  !> dummy argument names. But the switch statement will remain no 
  !> matter what we do, because the storage type is static.
  !>
  impure elemental subroutine serialize_program(str)
    type(shape_storage), intent(in) :: str

    select case(str%ishape)
    case(ICIRCLE)
      call serialize_circle(str%data(1))
    case(IRECTANGLE)
      call serialize_rectangle(str%data(1),str%data(2))
    case default
      error stop "Shape not available..."
    end select

  contains

    subroutine serialize_circle(radius)
      real, intent(in) :: radius
      print *, "CIRCLE    ", radius
    end subroutine

    subroutine serialize_rectangle(width,heigth)
      real, intent(in) :: width, heigth
      print *, "RECTANGLE ", width, heigth
    end subroutine

  end subroutine

end module

!>
!> Shape strategy (this time using pImpl idiom, with static procedures)
!>
module shape_base_module

use shape_implementation

implicit none
private

public :: shape_base, circle, rectangle

type :: shape_base
  private
  ! Essentially our "pointer to implementation" or pImpl
  ! Since it is allocatable, guaranteed memory safe.
  type(shape_storage), allocatable :: storage
contains
  !> The type-bound procedures just wrap the strategies
  procedure, public :: draw => draw_shape
  procedure, public :: serialize => serialize_shape
end type

contains

  !> 
  impure elemental subroutine draw_shape(shape)
    class(shape_base), intent(in) :: shape
    call draw_program(shape%storage)
  end subroutine
  impure elemental subroutine serialize_shape(shape)
    class(shape_base), intent(in) :: shape
    call serialize_program(shape%storage)
  end subroutine

  function circle(radius) result(s)
    real, intent(in) :: radius
    type(shape_base) :: s
    s%storage = circle_storage(radius)
  end function

  function rectangle(width,heigth) result(s)
    real, intent(in) :: width,heigth
    type(shape_base) :: s
    s%storage = rectangle_storage(width,heigth)
  end function

end module

!> Public-facing shape module
!>
!>   - Provide single entry point to the entire type hierarchy
!>   - Make names more manageable by renaming
!>
module shapes

  !> The base type (not abstract this time)
    use shape_base_module, only: shape => shape_base

  !> The child types are really just "functions", the implementation
  !> of these are tightly coupled with the shape_base storage
  !> representation. Also these are not type names anymore, but functions
    use shape_base_module, only: circle
    use shape_base_module, only: rectangle

end module

program process_shapes

  use shapes
  implicit none

  write(*,*) "Process shapes"
  call process_mixed_shapes()

contains

subroutine process_mixed_shapes()

  type(shape), allocatable :: collage(:)

  ! Look what has happened, we can have an array of mixed types :)
  ! Ain't that nice?

  collage = [ &
    circle(12.), &
    rectangle(12.,10.), &
    circle(6.) &
  ]

  call collage%serialize()  ! The elemental routines, still works, hooray!

  ! The price we've paid is the shape implementations are
  ! tightly coupled and rigid. 

end subroutine

end program
(Pseudo) Type Erasure
!> shapes_type_erasure.F90

!!
!! Shapes, but this time with something closer to type erasure
!!

! Instead of the top-down hierarchy originating from inheritance,
! we start bottom up this time.
!
! The shape modules are defined completely independently, and
! can also be used independently. No polymorphism or pointers, just
! plain derived types and procedures.
!
module shape_circle
   implicit none
   private
   public :: circle, draw, serialize
   type :: circle
      real :: radius
   end type
   interface draw
      module procedure draw_circle
   end interface
   interface serialize
       module procedure serialize_circle
   end interface
contains
   subroutine draw_circle(c)
      type(circle), intent(in) :: c
      write(*,'(A,G0,A)') "Drawing CIRCLE: { R = ", c%radius, "}"
   end subroutine
   subroutine serialize_circle(c)
      type(circle), intent(in) :: c
      print *, "CIRCLE ", c%radius
   end subroutine
end module

module shape_rectangle
   implicit none
   private
   public :: rectangle, draw, serialize
   type :: rectangle
      real :: width, heigth
   end type
   interface draw
       module procedure draw_rectangle
   end interface
   interface serialize
       module procedure serialize_rectangle
   end interface
contains
   subroutine draw_rectangle(r)
      type(rectangle), intent(in) :: r
      write(*,'(A,G0,A,G0,A)') "Drawing RECTANGLE { W = ", r%width, &
                                       ", H = ", r%heigth, "}"

   end subroutine
   subroutine serialize_rectangle(r)
      type(rectangle), intent(in) :: r
      print *, "RECTANGLE ", r%width, r%heigth
   end subroutine
end module

! We would still like to operate on collections of shapes,
! but let's try something different -> type-erasure (not quite, but close)
! 
module shapes

   use shape_circle
   use shape_rectangle
   
   implicit none
   private

   !> "Generic" shape type
   public :: shape

   !> Concrete types
   public :: circle, rectangle

   !> Operations
   public :: serialize
   public :: draw

   type :: shape
      !private
      class(*), allocatable :: value
   contains
      procedure :: serialize => serialize_generic
      procedure :: draw => draw_generic
   end type

   !> We should be able to call the same procedures on the
   !> generic shape, as we do on the simple primitives

   interface serialize
      module procedure serialize_generic
   end interface

   interface draw
      module procedure draw_generic
   end interface

   !> To build a generic shape we overload the structure constructor
   !> (this is also needed due to the private declaration)
   !
#ifdef __GFORTRAN__
   interface shape
      module procedure shape_from_circle
      module procedure shape_from_rectangle
      module procedure shape_from_shape
   end interface
#endif

   interface operator(.shape.)
      module procedure shape_from_circle
      module procedure shape_from_rectangle
      module procedure shape_from_shape
   end interface

contains

   !> We need these functions to convert the specific shapes
   !> into unlimited polymorphic variables
   !>
   !> Actually, ifort doesn't even need these, but gfortran does.
   !>
   pure function shape_from_circle(c) result(s)
      type(circle), intent(in) :: c
      type(shape) :: s
      s%value = c
   end function
   pure function shape_from_rectangle(r) result(s)
      type(rectangle), intent(in) :: r
      type(shape) :: s
      s%value = r
   end function
   pure function shape_from_shape(s) result(res)
      type(shape), intent(in) :: s
      type(shape) :: res
      res = s
   end function

   !> We can't avoid select type, however instead of having to 
   !> use select_type in the client code, the select types are now
   !> all in one place, and not in separate files.
   !> If you had many primite types, you could easily write a Fypp macro
   !> to do the unrolling for you.
   !>
   impure elemental subroutine serialize_generic(s)
      class(shape), intent(in) :: s
      associate(value => s%value)
         select type (value)
            type is (circle)
               call serialize(value)
            type is (rectangle)
               call serialize(value)
!            type is (shape)
!               call serialize(value)    ! nvfortran - recursive subroutine error
            class default
               !> In the present module, it should be impossible to
               !> reach this block.
               !> You could however use this block to your advantage if 
               !> you have a shape which doesn't support serialization.
               error stop "Sorry, don't know how to serialize that..."
         end select
      end associate
   end subroutine

   impure elemental subroutine draw_generic(s)
      class(shape), intent(in) :: s
      associate(value => s%value)
         select type (value)
            type is (circle)
               call draw(value)
            type is (rectangle)
               call draw(value)
!            type is (shape)
!               call draw(value)  ! nvfortran, recursive subroutine error 
            class default
               error stop "Sorry, don't know how to draw that..."
         end select
      end associate
   end subroutine

end module


!> Let's see where the use of interfaces and generics 
!> have brought us

program process_shapes

   use shapes
   implicit none

   call demo1()
   call demo2()

contains

   subroutine demo1()
      type(shape) :: shape_scalar
      type(shape) :: shape_array(3)

      write(*,'(A,/)') "Demo 1"

      !> We can construct a scalar shape by chaining the structure
      !> constuctors
      shape_scalar = shape(circle(42.))
      call serialize(shape_scalar)

      !> To construct an array of shapes, it would be nice if we could
      !> use an an array constructor with automatic conversion:
      !
      ! shape_array = [ shape :: &
      !     circle(10.), &
      !     rectangle(4.,5.), &
      !     circle(12.)]
      !
      !> Unfortunately, this doesn't work currently, and leads the the
      !> following errors:
      !>    gfortran - Error: Cannot convert TYPE(circle) to TYPE(shape)
      !>    ifort - Catastrophic error: **Internal compiler error**

      ! So instead we just assign the shapes one by one
      ! (again it would be nice to have an implicit conversion)
      shape_array(1) = shape(circle(10.))
      shape_array(2) = shape(circle(5.))
      shape_array(3) = shape(rectangle(width=12.,heigth=14.))
      
      ! Since the procedure is elemental, we can easily serialize
      ! the whole array
      call serialize(shape_array)
      ! or
      call shape_array%serialize()

      ! We can copy an existing shape, preserving the shape
      shape_scalar = shape_array(1)
      call serialize(shape_scalar)

      ! And we can also instantiate a shape from another shape
      ! (on the condition we overloaded the structure constructor)
      shape_scalar = shape(shape_array(3))
      call serialize(shape_scalar)
   end subroutine

   subroutine demo2()

      type(shape), allocatable :: scrapbook(:)

      write(*,'(/,A,/)') "Demo 2"


      scrapbook = [shape(circle(10.0))]
      scrapbook = [scrapbook, shape(rectangle(12.,12.))]

      call draw(scrapbook)

   end subroutine

end program

Here’s what the final Fortran program looks like for the “type erasure” version:

program draw_shapes
use shapes
implicit none

type(shape), allocatable :: s(:)

allocate(s(3))

s(1) = (.shape. circle(2.0))
s(2) = (.shape. rectangle(1.5,1.5))
s(3) = (.shape. circle(4.2))

call draw(s)

deallocate(s)

end program
$ gfortran -o draw_shapes draw_shapes.F90 
$ ./draw_shapes 
Drawing CIRCLE: { R = 2.00000000}
Drawing RECTANGLE { W = 1.50000000, H = 1.50000000}
Drawing CIRCLE: { R = 4.19999981}

I was quite surprised how easily the “Type Erasure” version came together. No inheritance at all. The shapes are completely decoupled from each other. The only short-coming is that in Fortran, adding a new shape requires adding a new section to the select type construct for each operation/interface you’d like to support. In C++ on the other hand, the template machinery takes care of this automatically, i.e. adding a new concrete shape requires zero changes of the generic Shape class.

The NVIDIA HPC SDK Fortran compiler (v22.7) compiles three out of four examples, however only one of them works correctly which, to my surprise, is the “Naive inheritance”. It seems to have some problems with structure constructors

I think there is a some potential to enhance the unlimited polymorphic variables in Fortran to enable easier and safer generic programming.

5 Likes

Nice example! with the caveat that all shapes have the same interface, I think you can remove most of the boilerplate using a hybrid of your first and last approaches:

module shapes
   use iso_fortran_env
   implicit none
   private

   type, public, abstract :: shape_base
       contains
       procedure(shape_base_draw), deferred :: draw
       procedure(shape_base_ser), deferred :: serialize
   end type shape_base

   type, public, extends(shape_base) :: shape
      class(shape_base), allocatable :: value
   contains
      procedure :: serialize => shape_serialize
      procedure :: draw => shape_draw
      procedure, private :: shape_assign
      generic :: assignment(=) => shape_assign
   end type shape

   ! The basic shapes
   type, public, extends(shape_base) :: circle
       real(real64) :: radius
       contains
       procedure :: draw => draw_circle
       procedure :: serialize => serialize_circle
   end type circle

   type, public, extends(shape_base) :: rectangle
       real(real64) :: width,height
       contains
       procedure :: draw => draw_rectangle
       procedure :: serialize => serialize_rectangle
   end type rectangle

   abstract interface
       subroutine shape_base_draw(c)
          import shape_base
          class(shape_base), intent(in) :: c
       end subroutine
       impure elemental subroutine shape_base_ser(c)
          import shape_base
          class(shape_base), intent(in) :: c
       end subroutine
   end interface

   interface shape
      module procedure shape_new
   end interface

contains
   subroutine draw_circle(c)
      class(circle), intent(in) :: c
      write(*,'(A,G0,A)') "Drawing CIRCLE: { R = ", c%radius, "}"
   end subroutine
   impure elemental subroutine serialize_circle(c)
      class(circle), intent(in) :: c
      print *, "CIRCLE ", c%radius
   end subroutine

   subroutine draw_rectangle(c)
      class(rectangle), intent(in) :: c
      write(*,'(A,G0,A,G0,A)') "Drawing RECTANGLE { W = ", c%width, &
                                       ", H = ", c%height, "}"
   end subroutine
   impure elemental subroutine serialize_rectangle(c)
      class(rectangle), intent(in) :: c
      print *, "RECTANGLE ", c%width, c%height
   end subroutine

   subroutine shape_draw(c)
      class(shape), intent(in) :: c
      if (allocated(c%value)) then
         call c%value%draw()
      else
         stop 'trying to draw an empty shape'
      endif
   end subroutine

   impure elemental subroutine shape_serialize(c)
      class(shape), intent(in) :: c
      if (allocated(c%value)) then
         call c%value%serialize()
      else
         print *, "EMPTY "
      endif
   end subroutine

   ! Shape initializer
   subroutine shape_assign(lhs,rhs)
       class(shape), intent(inout) :: lhs
       class(shape_base), intent(in) :: rhs
       if (allocated(lhs%value))deallocate(lhs%value)
       allocate(lhs%value,source=rhs)
   end subroutine shape_assign

   ! Shape initializer
   type(shape) function shape_new(rhs) result(lhs)
       class(shape_base), intent(in) :: rhs
       allocate(lhs%value,source=rhs)
   end function shape_new

end module shapes

!> Let's see where the use of interfaces and generics
!> have brought us

program process_shapes

   use shapes
   implicit none

   call demo1()
   call demo2()

contains

   subroutine demo1()
      type(shape) :: shape_scalar
      type(shape) :: shape_array(3)

      write(*,'(A,/)') "Demo 1"

      !> We can construct a scalar shape by chaining the structure
      !> constuctors
      shape_scalar = circle(42.)
      call shape_scalar%serialize()

      !> To construct an array of shapes, it would be nice if we could
      !> use an an array constructor with automatic conversion:
      !
      ! shape_array = [ shape :: &
      !     circle(10.), &
      !     rectangle(4.,5.), &
      !     circle(12.)]
      !
      !> Unfortunately, this doesn't work currently, and leads the the
      !> following errors:
      !>    gfortran - Error: Cannot convert TYPE(circle) to TYPE(shape_base)
      !>    ifort - Catastrophic error: **Internal compiler error**

      ! So instead we just assign the shapes one by one
      ! (again it would be nice to have an implicit conversion)
      shape_array(1) = circle(10.)
      shape_array(2) = circle(5.)
      shape_array(3) = rectangle(width=12.,height=14.)

      ! Since the procedure is elemental, we can easily serialize
      ! the whole array
      call shape_array%serialize()

      ! We can copy an existing shape, preserving the shape
      shape_scalar = shape_array(1)
      call shape_scalar%serialize()

      ! And we can also instantiate a shape from another shape
      ! (on the condition we overloaded the structure constructor)
      shape_scalar = shape_array(3)
      call shape_scalar%serialize()
   end subroutine

   subroutine demo2()

      type(shape), allocatable :: scrapbook(:)

      write(*,'(/,A,/)') "Demo 2"


      scrapbook = [shape(circle(10.0))]
      scrapbook = [scrapbook, shape(rectangle(12.,12.))]

      call scrapbook%serialize()
   end subroutine

end program
  • Full inheritance => no need to keep redefining interfaces or using select types
  • Just a bit verbose array constructor (at least with gfortran)

*** EDIT ***

Small improvement: to prevent unwanted nested types, without loss of generality, the assignment routine can be modified as

   subroutine shape_assign(lhs,rhs)
       class(shape), intent(inout) :: lhs
       class(shape_base), intent(in) :: rhs
       if (allocated(lhs%value))deallocate(lhs%value)
       ! Prevent nested derived type
       select type (rhsT=>rhs)
          class is (shape);  if (allocated(rhsT%value)) allocate(lhs%value,source=rhsT%value)
          class default;     allocate(lhs%value,source=rhsT)
       end select
   end subroutine shape_assign

Array handling also works:

   subroutine demo3()

     type(shape), allocatable :: array(:),chunk(:)
     integer :: i

     write(*,'(/,A,/)') "Demo 3"

     allocate(array(0))
     do i=1,10
        array = [array,shape(circle(real(i)))]
     end do

     chunk = array(2:8:2)
     call chunk%serialize()

   end subroutine demo3

produces

Demo 3

 CIRCLE    2.0000000000000000     
 CIRCLE    4.0000000000000000     
 CIRCLE    6.0000000000000000     
 CIRCLE    8.0000000000000000     
2 Likes

Thanks Federico for your additional example. If I understand correctly, the core design choice is to encapsulate the the abstract base class in the extendable base class (I guess in C++ they would call this a pointer (unique_ptr) to an abstract base class).

From past C++ courses, I have observed one needs to be very careful when he chooses to overload assignment. Especially if the types contain pointers, it can become a nightmare to reason about. I’d prefer to rely on intrinsic assignment if possible. I can see the benefit, by overloading assignment, no explicit conversion (construction) is needed to assign the polymorphic shape items:

      shape_array(1) = circle(10.)
      shape_array(2) = circle(5.)
      shape_array(3) = rectangle(width=12.,height=14.)

These type of exercises are valuable to discover the pros and cons of different patterns, both in terms of coupling, boilerplate, and performance. I guess all languages have a degree of isomorphism, there are multiple ways to achieve the same thing. I’m not sure how strict the Fortran standard is in terms the processor following the syntax strictly, or weather it is allowed to take “shortcuts” as long as the observable behavior remains the same.

I think the cool feature of the type-erasure pattern is there is no inheritance at all. The ugly part is the need for “manual” reificiation, that is, re-establishing the type from the unlimited polymorphic variable. It looks even weirder, given that once we are in the block where the type is known, we use a generic interface… I feel like something is missing in the language, so that this could be simplified. I’m curious if the generic committee have seen or put this pattern to use before (cc @everythingfunctional, @certik).

If the number of shapes and operations became large, to save some effort from writing boilerplate, a preprocessor template (e.g. using Fypp) can be used instead:

#! Print subroutine attributes such as impure and elemental
#!
#:def subroutine_preamble(**kwargs)
#:if kwargs.get("impure")
impure &
#:endif
#:if kwargs.get("elemental")
elemental &
#:endif
#:enddef

#! Generic shape subroutine
#!
#! Use this template to instantiate subroutines for a set of derived types
#! describing shapes, and the shared operations specified by a generic 
#! procedure interface.
#!
#:def generic_subroutine(operation,*types,**kwargs)
!>
!> Generic operation - ${operation}$
!>
$:subroutine_preamble(**kwargs)
subroutine ${operation}$_generic(s)
   class(shape), intent(in) :: s
   associate(value => s%value)
   select type(value)
#:for t in types
   type is (${t}$)
      call ${operation}$(value)
#:endfor
   class default
      error stop "ERROR: don't know how to ${operation}$ received shape"
   end select
   end associate
end subroutine
#:enddef

The actual procedure generation loop is now:

#:set shapes = ["circle", "rectangle"]
#:set subroutines = ["draw", "serialize"]

#:for sub in subroutines
$:generic_subroutine(sub,*shapes, &
    impure=True, &
    elemental=True)
#:endfor

The expanded templates are displayed in the drop-down box:

Generic subprograms via preprocessing
!>
!> Generic operation - draw
!>
impure &
elemental &
subroutine draw_generic(s)
   class(shape), intent(in) :: s
   associate(value => s%value)
   select type(value)
   type is (circle)
      call draw(value)
   type is (rectangle)
      call draw(value)
   class default
      error stop "ERROR: don't know how to draw received shape"
   end select
   end associate
end subroutine
!>
!> Generic operation - serialize
!>
impure &
elemental &
subroutine serialize_generic(s)
   class(shape), intent(in) :: s
   associate(value => s%value)
   select type(value)
   type is (circle)
      call serialize(value)
   type is (rectangle)
      call serialize(value)
   class default
      error stop "ERROR: don't know how to serialize received shape"
   end select
   end associate
end subroutine

I don’t know, I’m no computer scientist but I think the cool idea is to have an extended derived type that contains an allocatable component of the abstract class. In this way, the “container” type is completely interchangeable with all others defined types, so you can define it type(shape) and use all the cool Fortran array facilities that you otherwise couldn’t.

On the drawback side, it won’t be super fast if you keep changing data, because of the allocations.

On the interesting side, there may be cool applications in lists with key/value pair types which could use the recursive definition.

1 Like

At the time I didn’t have a name for, but a past post from @Arjen also fits under the spirit of type erasure. In that thread the challenge was to write a generic “reverse” subroutine.

Reversing an array, only requires that the types are assignable so you can swap their contents:

subroutine reverse( array, assign )
    class(*), dimension(:), intent(inout), pointer     :: array  
       ! (25/9/2022, is pointer attribute needed?)
    interface
        subroutine assign( a, b )
            class(*), intent(out) :: a
            class(*), intent(in)  :: b
        end subroutine assign
    end interface

    class(*), allocatable                              :: tmp
    integer                                            :: i, j

    allocate( tmp, mold=array(1) )

    do i = 1,size(array)/2
        j        = size(array) + 1 - i
        call assign( tmp, array(i) )
        call assign( array(i), array(j) )
        call assign( array(j), tmp )
    enddo
end subroutine reverse
end module reverse_array

For each type you’d like to swap, you need a subroutine such as:

subroutine assign_person( a, b )
    class(*), intent(out) :: a
    class(*), intent(in)  :: b
    select type( a )
        type is (person)
            select type( b )
                type is (person)
                    a = b
            end select
    end select
end subroutine assign_person

It’s not difficult to see, you just replace person with any type, and you can reuse the reverse routine. It seems very close to what C++20 concepts are for:

// reverse.cpp

#include<concepts>
#include<vector>
#include<iostream>
#include<string>

template<typename T>
concept Assignable = requires(T a, T b) {
    a = b;
};

template<Assignable T>
void reverse(std::vector<T> &array) {

    int n = array.size();
    for (int i = 0; i < n/2; ++i) {
        int j = n - i - 1;
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }    
}

template<class Array>
void show(std::string title, Array c) {
    std::cout << title << " = [ ";
    for(int i = 0; i < c.size(); ++i) {
        std::cout << c[i] << " ";
    }
    std::cout << "]\n";
}

int main(int argc, char const *argv[]) {

    std::vector<int> arr{1,2,3,4,5};
    
    show("      initial",arr);
    reverse(arr);    
    show("after reverse",arr);

    return 0;
}

Well put. That’s the essence of it - you can build on the built-in array semantics, but with polymorphic items.

Klaus Iglberger mentions something about this; if you are working with small objects then value-semantics are your friend. If you are working with large objects (e.g. huge arrays of doubles) and you need to avoid expensive copies, then reference-semantics are unavoidable. The language gives you both, but you need to use them for the right purpose.

1 Like

According to section 15.5 in Metcalf, Reid, Cohen (the “red book”), you can achieve the same thing with intrinsic assignment:

lhs%value = rhs

with the benefit that if the lhs%value is already allocated with the same type (and shape and other parameters), no reallocation is done (i.e. only the values are copied). This can potentially improve performance. (A fictive example, say you process shapes in batches, when you receive the second or further batches the virtual tables and containers already exist and don’t need to be allocated, but you just overwrite the old values with the newly received batch).

I’ve got an example of the “shapes idea” that I used in a course once here.

I’m not sure I’ve fully understood the type-erasure pattern, but I suspect it won’t be possible in Fortran the way they seem to be able to do it in C++, even with the upcoming template features. It’s worth some exploration though. The thing is that Fortran can’t do generic resolution at run-time, and if you’ve “erased” the type, it has nothing to go on for the generic resolution at compile-time either, so you’re kind of forced to use inheritance.