What is benefit of PASS in type-bound procedures?

In Fortran we can control the passed-object dummy argument of type-bound procedures:

module engine_mod

type :: engine_type
contains
   procedure(ignite_fun), pass(engine) :: ignite
end type

contains
   subroutine ignite(params,engine)
      real :: params(3)
      class(engine_type) :: engine
      print *, "Igniting engine with params: ", params
   end subroutine
end module

This is different from languages like C++ which uses an implicit object parameter, the so-called this pointer, or from Python method objects where it appears as the first argument of the method and is usually named self (this is not required).

As explained in the Fortran interpretation document (J3/24-007, sec. 7.5.4.5, pg. 96),

If a procedure is bound to several types as a type-bound procedure, different dummy arguments might be the
passed-object dummy argument in different contexts.

This means if we have a procedure using multiple different derived type classes, those procedures can be bound to multiple methods.

Is there any special use case of this feature? Is it just for the convenience of the caller? What would be a good situation to use it?

1 Like

You can also use it to keep the interface the same, when a type does not appear as the first argument. For instance, a routine that was written before the standard offered type-bound procedures.

Isn’t that a property the NOPASS gives you?

The PASS only comes into play when you invoke a method through the obj%tbp() mechanism.

Since there are no reserved keywords in Fortran, a this (or self or me, etc.) couldn’t be considered, so the passed-object had to be explicit —besides Python, Go comes to mind as another example of explicit passed-object (where it’s called “receiver”).

The pass attribute may useful for overloading or defining operators:

type :: t
contains
    generic :: operator(+) => t_plus_t, t_plus_i, i_plus_t
    procedure :: t_plus_t, t_plus_i
    procedure, pass(rhs) :: i_plus_t
end type

In C++ you would have to use a friend function for the i_plus_t case.

Also, there might be a debate of whether inout|out arguments should go at the beginning (command order) or at the end (processing order), so the pass attribute helps with your preferences.

2 Likes

The case of reusing the procedure in multiple derived types seems less useful, so it might just be a side-effect of the way OOP was introduced into the language?

module mod1
    type :: t1
    contains
        generic :: operator(//) => t1_concat_t2
        procedure :: t1_concat_t2
    end type

    type :: t2
    contains
        generic :: operator(//) => t1_concat_t2
        procedure, pass(rhs) :: t1_concat_t2
    end type

contains
    pure function t1_concat_t2(lhs, rhs) result(str)
        character(:), allocatable :: str
        class(t1), intent(in) :: lhs
        class(t2), intent(in) :: rhs
        ...
    end function
end module

Typically I don’t use operator overloading a lot, and if possible I limit the use to non-polymorphic types. But I can see why that is needed and it makes sense franly.

As you remark, the case of reusing a procedure in multiple derived types is a bit strange.
What irks me a bit about this feature is the strong coupling it introduces.

Here is a potential use-case I’ve found for providing default settings, thematically related to the threads:

The example works with gfortran, ifort, ifx, flang, nvfortran and nagfor.

! linear_operators_demo.f90 --
!   Example of using passed-object dummy arguments as a means
!   of implementing a default fallback method.
!
!   This produces tight coupling of the problem class (linop) and the
!   solver class (krylov_method).
!
module linear_operators

implicit none
private

public :: dp, linop, matvec, preconditioner
public :: krylov_method, bicgstab
public :: linsolve

integer, parameter :: dp = kind(1.0d0)

! A linear operator of the form y = op(x)
type, abstract :: linop
    integer :: n = 0
contains
   procedure(apply_sub), deferred :: apply
end type

abstract interface
    subroutine apply_sub(op,x,y)
        import linop, dp
        class(linop), intent(in) :: op
        real(dp), intent(in) :: x(op%n)
        real(dp), intent(out) :: y(op%n)
    end subroutine
end interface

! Applies the y = A x operation, where A is a dense matrix
type, extends(linop) :: matvec
    ! Dense matrix, for the sake of this experiment
    real(dp), allocatable :: A(:,:)
contains
    procedure :: apply => apply_matvec

! N.b.: we could also make this method non_overridable
    procedure, pass(A) :: solve => solve_using_method
end type

! Applies the P = M^{-1} operation, by default this is just M = I
type, extends(linop) :: preconditioner
contains
    procedure :: apply => default_pc
end type

! Applies the A^{-1} operation, using a Krylov process
type, abstract :: krylov_method
contains
    procedure(solve_sub), deferred, pass(method) :: apply
end type

abstract interface
    subroutine solve_sub(A,b,x,P,method,info)
        import matvec, preconditioner, krylov_method, dp
        class(matvec), intent(in) :: A
        real(dp), intent(in) :: b(:)
        real(dp), intent(inout) :: x(:)
        class(preconditioner), intent(in), optional :: P
        class(krylov_method), intent(inout), optional :: method
        integer, intent(out) :: info
    end subroutine
end interface

! One particular Krylov method
type, extends(krylov_method) :: bicgstab
contains
    procedure, pass(method) :: apply => apply_bicgstab
end type

! Add to generic overload set, for solving different problems
interface linsolve
    module procedure :: solve_using_method
end interface

contains

    ! Dense matrix-vector product
    subroutine apply_matvec(op,x,y)
        class(matvec), intent(in) :: op
        real(dp), intent(in) :: x(op%n)
        real(dp), intent(out) :: y(op%n)
        y = matmul(op%A,x)
    end subroutine

    ! y = I x
    subroutine default_pc(op,x,y)
        class(preconditioner), intent(in) :: op
        real(dp), intent(in) :: x(op%n)
        real(dp), intent(out) :: y(op%n)
        y = x
    end subroutine

    subroutine apply_bicgstab(A,b,x,P,method,info)
        class(matvec), intent(in) :: A
        real(dp), intent(in) :: b(:)
        real(dp), intent(inout) :: x(:)
        class(preconditioner), intent(in), optional :: P
        class(bicgstab), intent(inout), optional :: method
        integer, intent(out) :: info

        ! this should always works in theory
        if (.not. present(method)) error stop "bicgstab method is not present."

        info = 0
        print *, "Calling fake bicgstab routine"

    end subroutine

    ! generic solve procedure with fallback to a default method
    subroutine solve_using_method(A,b,x,P,method,info)
        class(matvec), intent(in) :: A
        real(dp), intent(in) :: b(:)
        real(dp), intent(inout) :: x(:)
        class(preconditioner), intent(in), optional :: P
        class(krylov_method), intent(inout), optional :: method
        integer, intent(out) :: info
        if (present(method)) then
            call method%apply(A,b,x,P,info)
        else
            info = 0
            print *, "Calling fallback method"
        end if
    end subroutine

end module


program linear_operator_demo

    use linear_operators
    implicit none

    type(preconditioner) :: eye
    type(matvec) :: A
    class(krylov_method), allocatable :: method

    integer, parameter :: n = 3
    real(dp) :: x(n), b(n)
    integer :: info

    allocate( bicgstab :: method )

    call method%apply(A,b,x,info=info)
    call A%solve(b,x,method=method,info=info)
    call A%solve(b,x,info=info)

    call linsolve(A,b,x,P=eye,method=method,info=info)

end program
$ gfortran -Wall linear_operators_demo.f90 
$ ./a.out
 Calling fake bicgstab routine
 Calling fake bicgstab routine
 Calling fallback method
 Calling fake bicgstab routine

Polymotphism is not that bad —the real problem is unnecessary inheritance.

Your example is interesting —although, the the passed-object being optional looks weird.

I thought so too; but then I thought about C and C++ - when you have a pointer argument, there is always a possibility it could be null.

I just created a smaller example, and it works consistently across compilers (Compiler Explorer) in the non-polymorphic case:

module test_mod
    implicit none
    type :: foo
    contains
        procedure, pass :: say_hello
        procedure, nopass :: say_goodbye
    end type
contains
    subroutine say_hello(f)
        class(foo), optional :: f
        if (present(f)) then
            print *, "hello from f"
        else
            print *, "hello from null()"
        end if
    end subroutine
    subroutine say_goodbye
        print *, "goodbye"
    end subroutine
end module

program test
    use test_mod
    type(foo), allocatable :: f
    call f%say_hello
    allocate(f)
    call f%say_hello
    call f%say_goodbye
end program

In the polymorphic case, the majority of compilers produce crashing executables:

program test
    use test_mod
! N.b. v-- class here
    class(foo), allocatable :: f
    call f%say_hello  ! <-- breaks on ifx, ifort, flang, nvfortran, and nagfor
    allocate(f)
    call f%say_hello
    call f%say_goodbye
end program

I guess the crash has to do with the fact that before allocation, the dynamic type of f isn’t yet established, meaning the virtual method table of the f instance isn’t populated either (the procedured could be overriden by a child type). If I add the non_overridable attribute to say_hello, then flang doesn’t crash which makes sense to me.

The rules for type-bound procedure overriding stipulate:

2 Likes