Fortran 2003/2008:优雅的默认参数?

19

在Fortran中,我们可以定义默认参数。然而,如果一个可选参数不存在,它也不能被设置。当使用带有默认值的关键字参数作为参数时,这会导致笨拙的结构,例如

PROGRAM PDEFAULT 

  CALL SUB
  CALL SUB(3)

CONTAINS 
  SUBROUTINE SUB(VAL)
    INTEGER, OPTIONAL :: VAL
    INTEGER :: AVAL ! short for "actual val"

    IF(PRESENT(VAL)) THEN
       AVAL = VAL
    ELSE 
       AVAL = -1   ! default value 
    END IF

    WRITE(*,'("AVAL is ", I0)') AVAL
  END SUBROUTINE SUB

END PROGRAM PDEFAULT

个人经验是,我经常会遇到在代码中意外地输入 VAL 而不是 AVAL 的问题,也就是接口中的变量名与代码中使用的初始化值之间的断开可能会引入运行时错误 - 更不用说这种初始化方式非常冗长了。

有没有更优雅的方法来使用带有默认值的可选参数?

示例 写起来更自然的方式可能是:

IF(NOT(PRESENT(VAL))) VAL = -1 

因为它避免了 VALAVAL 的混淆。但它不是有效的,可能是因为Fortran通过引用传递参数,因此如果CALL语句中没有出现VAL,则不会关联任何内存到VAL,并且VAL = -1将导致段错误。


1
有趣的细节:无论是GFortran还是Intel Fortran,在使用可选参数而没有通过“present()”进行保护时,都不会提供任何编译时警告。两者在运行时只会简单地失败并导致segfault。 - kdb
7个回答

15
你描述了情况相当不错。我所知道的没有其他方法,这是符合标准的。人们经常使用具有类似命名的局部变量的模式。另一种选择是在每个地方都放置if (present()) else ,但那样很笨拙。
重点是它们是可选参数,而不是默认参数。Fortran没有默认参数。也许默认参数会更好,但这不是80年代准备Fortran 90时委员会成员们的选择。

8

在调查此事时,我发现实际上可以使用 OPTIONALVALUE 属性来执行类似建议示例的操作(至少在 gfortran 中可以,不确定其他编译器如何处理)。例如:

PROGRAM PDEFAULT 

  CALL SUB
  CALL SUB(3)

CONTAINS 
  SUBROUTINE SUB(VAL)
    INTEGER, OPTIONAL,VALUE :: VAL

    IF(.NOT. PRESENT(VAL)) VAL = -1 ! default value

    WRITE(*,'("VAL is ", I0)') VAL
  END SUBROUTINE SUB

END PROGRAM PDEFAULT

这是在gfortran 4.9版本中实现的。以下是参数传递规则文档中相关的解释:
对于可选的虚拟参数,缺少的参数用NULL指针表示,但INTEGER、LOGICAL、REAL和COMPLEX类型的标量虚拟参数具有VALUE属性。对于这些参数,使用一个隐藏的布尔参数(logical(kind=C_bool),value)来指示参数是否存在。
我还发现这个讨论很有趣作为历史背景。
也许更有知识的人可能会评论是否这样做是一个坏主意(除了依赖编译器),但至少表面上看起来像一个不错的解决方法。
请注意,这种行为不是Fortran标准的一部分,而是取决于给定编译器的实现。例如,当使用ifort(版本16.0.2)时,示例代码会导致段错误。

3
我认为这不是有效的Fortran代码(这并不意味着在某些编译器/运行时中它不能按预期执行)。如果val不存在,则不允许引用/定义它。这是if语句。标准对于非存在虚参的使用限制没有提到value属性。"definable anonymous copy"仅适用于具有value属性的存在虚参情况下。[希望有人能证明我是错的,不过]。 - francescalus
嗯,我认为你是对的。早上我还有点担心,但后来Bugzilla的讨论帖让我信服了。然而,在那个页面上他们并没有建议如果参数不存在就允许引用该参数。 - Vladimir F Героям слава
我同意这种行为既不是必需的,也不是标准的一部分。从bugzilla讨论中我了解到,标准允许使用OPTIONALVALUE属性,但没有指导如何实现。由于在gfortran中,带有VALUE属性的参数是按值传递的,因此我认为当可选参数缺失时,变量仍然被分配但未初始化(这就是为什么它们需要一个隐藏参数)。 - Gabe
2
另外,我已经使用ifort测试了代码,但它会出现段错误。我将在答案中澄清这不是标准的一部分,而是与实现相关的。 - Gabe
4
不标准的部分是当VAL不存在时进行赋值。在我的代码中,我使用了原始帖子中的技术。我要评论一下,“省略可选参数的默认值”是正在考虑纳入下一个标准修订版(目前称为F202X)的功能。 - Steve Lionel

3
Fortran标准库(https://github.com/fortran-lang/stdlib)提供了一个名为optval的函数,例如在stdlib_logger中使用:
subroutine add_log_file( self, filename, unit, action, position, status, stat )
    ...
    character(*), intent(in), optional :: action
    ...
    character(16)  :: aaction
    ...
    aaction = optval(action, 'write')
    ...
end subroutine add_log_file

因此,他们表示“实际”值的方式是在前面加上a

我个人认为,在后面添加_的选项更好,因为可选值在调用签名中会有视觉标记。


很好。值得一提的是,人们可以自己编写这样的函数,它只包含已经讨论过的if条件语句,没有新的魔法。 - Vladimir F Героям слава
1
我研究了这个库的源代码。我的结论是,它大多数情况下展示了Fortran需要类型通用(“模板”)编程的原因... - kdb

1

虽然在大多数情况下我不建议这样做(事实上,在某些情况下,您无法这样做),但有时候可以使用接口来提供单个入口点,使多个例程具有不同的必需参数,而不是使用可选参数。例如,您可以编写以下代码:

MODULE subs
  implicit none
  public :: sub

  interface sub
    module procedure sub_default
    module procedure sub_arg
  end interface
 contains
  SUBROUTINE SUB_arg(VAL)
    INTEGER :: VAL
    WRITE(*,'("VAL is ", I0)') VAL
  END SUBROUTINE SUB_arg

  SUBROUTINE SUB_default
     integer, parameter :: default = 3
     CALL SUB_arg(default)
  END SUBROUTINE SUB_default
END MODULE SUBS

PROGRAM test
   use subs, only: sub
   call sub
   call sub(5)
END PROGRAM TEST

再次强调,我不建议采用这种方法,但是考虑到提供类似默认值的替代方式,我还是决定把它包含在内。


为什么你不推荐这个呢?特别是当有很多可选参数相互作用时,这是一个合理的方法。或者当过程已经是通用的时,这个解决方案将不会增加太多额外的样板代码。 - knia

1
另一种可能性是使用关联块,将局部变量名与可选参数同名的变量相关联,例如:
SUBROUTINE SUB(VAL)
INTEGER, OPTIONAL :: VAL
INTEGER :: AVAL ! short for "actual val"

IF (PRESENT(VAL)) THEN
    AVAL = VAL
ELSE 
    AVAL = -1   ! default value 
END IF

ASSOCIATE (VAL => AVAL)
    WRITE(*,'("VAL is ", I0)') VAL
END ASSOCIATE

END SUBROUTINE SUB

这并不是最理想的方法,但允许您在参数和例程体中使用相同的变量名。我不敢想象为了应对可选参数的默认值不足而编写的混乱代码量 - F202X,我们来吧。


这当然有一些吸引人的特点,但也许你可以在回答中扩展一下这种方法在更普遍情况下的限制? - francescalus
限制在于使用关联语句 - 对可分配和指针变量的限制。此外,如果参数是 INTENT(OUT)(或没有明确的意图),则需要在关联块之后添加相应的 IF (PRESENT(VAL)) VAL = AVAL。虽然这一切有点混乱。对于可选参数的默认值会更好。 - nocaster60
你愿意把这些点加入到你的回答中吗? - francescalus

0

我希望Fortran能支持像流行语法一样的东西

subroutine mysub( x, val = -1 )
integer, optional :: val

或者更像Fortran风格的写法

subroutine mysub( x, val )
integer, optional :: val = -1     !! not SAVE attribute intended

但是据我所知(截至2016年)似乎不被支持。因此,用户需要自己进行一些解决方法...

在我的情况下,经过反复尝试,我最终选择给可选的虚拟参数添加一个下划线,这样做类似于 (*)

subroutine mysub( x, val_)
integer, optional :: val_
integer val

其他人似乎喜欢相反的模式(即,虚拟变量 => sep,本地变量 => sep_,例如请参见split()StringiFor中)。如this line所示,设置默认值的最短方式是

val = -1 ; if (present(val_)) val = val_

但是因为即使这行代码有点啰嗦。我通常会定义一个宏,像这样:

#define optval(x,opt,val) x = val; if (present(opt)) x = opt

在一个公共的头文件中定义并使用它

subroutine mysub( x, val_, eps_ )
    integer :: x
    integer, optional :: val_
    real, optional :: eps_
    integer  val
    real     eps
    optval( val, val_, -1 )
    optval( eps, eps_, 1.0e-5 )    

    print *, "x=", x, "val=", val, "eps=", eps
endsubroutine

...
call mysub( 100 )
call mysub( 100, val_= 3 )
call mysub( 100, val_= 3, eps_= 1.0e-8 )

然而,我认为这还远远不够优雅,仅仅是为了使其稍微少出错一些(通过在子程序的主体中使用所需的变量名)。

对于非常“大”的子程序,另一个解决方法可能是传递一个包含所有剩余关键字参数的派生类型。例如:

#define getkey(T) type(T), optional :: key_; type(T) key; if (present(key_)) key = key_

module mymod
    implicit none

    type mysub_k
        integer  :: val = -1
        real     :: eps = 1.0e-3
    endtype
contains

subroutine mysub( x, seed_, key_ )
    integer :: x
    integer, optional :: seed_
    integer :: seed
    getkey(mysub_k)   !! for all the remaining keyword arguments
    optval( seed, seed_, 100 )    

    print *, x, seed, key% val, key% eps
endsubroutine

endmodule

program main
    use mymod, key => mysub_k

    call mysub( 10 )
    call mysub( 20, key_= key( val = 3 ) )
    call mysub( 30, seed_=200, key_= key( eps = 1.0e-8 ) )  ! ugly...
endprogram

这可能与某些动态语言在幕后执行的操作非常接近,但是这种形式仍然远离优雅...


(*) 我知道使用 CPP 宏通常被认为是丑陋的,但在我看来,这取决于它们的使用方式;如果它们仅限于有限的 Fortran 语法扩展,我认为使用是合理的(因为 Fortran 中没有元编程功能);另一方面,定义程序相关的常量或分支可能应该避免。此外,我想使用 Python 等更灵活的预处理器(例如 PreForM.pyfypp 等)可能会更加强大,例如允许像 subroutine sub( val = -1 ) 这样的语法。

第一个解决方案是我在原问题中发表的-更改命名约定并不能真正使其成为新模式。第二个解决方案很遗憾是无效的代码,一旦有多个可选参数,因为fortran不允许混合声明和执行代码-虽然对于单个可选参数,使用宏可能不足以证明其晦涩性。 - kdb
由于当前的Fortran标准不允许任何真正的“解决方案”(如Vladimir所建议的),因此除了使用一个类似名称的局部变量外,没有其他办法,我认为您正在寻找一些系统化(更少出错)的解决方法。关于第二点,上面的代码是有效的(您可以尝试一下),您也可以在key_之前添加其他通常的可选变量。关键是setkey()应该在所有其他声明之后,但这是自然的,因为这种类型的关键字参数通常在所有可选参数之后出现。 - roygvib
此外,我的目的只是展示我的实践,而不是倡导其使用。更重要的是,我的意图是强调当前Fortran语法在某些方面受到限制,并迫使用户编写相当冗长的代码。 - roygvib
我添加了更多的代码来混合可选和“关键字”参数,但毕竟这些代码相当丑陋(尽管我经常使用第一个optval(),但我不会使用第二个)。 - roygvib
2
在“F202X”中考虑的一项内容是添加一个能够实现此目的的东西。目前还没有提出任何语法建议。 - Steve Lionel

0
这里有一个优雅的解决方案(即短小、清晰、符合标准):
  subroutine sub(val)
    integer, optional :: val

    write(*,'("aval is ", i0)') val_or_default(val, default=-1)
  end subroutine

  integer function val_or_default(val, default)
    integer, optional, intent(in) :: val
    integer, intent(in) :: default

    if (present(val)) then  ! False if `val` is is not present in `sub`.
      val_or_default = val
    else
      val_or_default = default
    endif
  end function

这个方法利用了可选参数仍然可以传递给函数的事实,即使它们不存在, 只要相应的虚拟参数也是可选的。

至少有一个通用的 val_or_default 实现在 GitHub 上适用于所有内置数据类型。 (他们称之为 optval。)


1
Optval已经在@greeeeeeen的答案中提到了。 - Vladimir F Героям слава

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接