本节摘要:过程是软件的最小执行单元,本节把它的每个决策点讲透:函数与子程序怎么分工,intent 与 value 如何把参数契约写进签名,pure 与 elemental 如何为并行与向量化开路,递归的栈成本怎么控制,以及泛型接口如何实现"同名多实现"的编译期分派。读完你能设计出接口清晰、可并行、可维护的过程。
Fortran 里两者并存,不是历史遗留,是语义分工:函数表达数学映射,核心特征是有返回值,理想状态无副作用;子程序表达动作,通过参数改变状态或执行输入输出。判断依据很简单:调用处要不要把结果嵌进表达式?要,用函数;只是"去做一件事",用子程序。
module calc_mod use iso_fortran_env, only: real64 implicit none contains ! 函数:计算距离,无副作用,可嵌进表达式 real(real64) function distance(x1, y1, x2, y2) result(d) real(real64), intent(in) :: x1, y1, x2, y2 d = sqrt((x2 - x1)**2 + (y2 - y1)**2) end function distance ! 子程序:推进粒子一步,改变对象状态 subroutine advance(x, y, vx, vy, dt) real(real64), intent(inout) :: x, y real(real64), intent(in) :: vx, vy, dt x = x + vx * dt y = y + vy * dt end subroutine advance end module calc_mod program proc_demo use calc_mod, only: distance, advance use iso_fortran_env, only: real64 implicit none real(real64) :: x, y, vx, vy, dt x = 0.0_real64; y = 0.0_real64; vx = 1.0_real64; vy = 0.0_real64; dt = 0.1_real64 call advance(x, y, vx, vy, dt) write(*,*) distance(0.0_real64, 0.0_real64, x, y) end program proc_demo
别在函数里做大量状态修改,也别把函数当子程序用——那是 bug 的温床。把函数当"查询",子程序当"命令",读代码的人才能预测行为。
intent 是 Fortran 最被低估的工程设施。intent(in) 承诺不修改参数——编译器据此检查、也据此优化;intent(out) 声明"仅用于输出,进入时无需初始化";intent(inout) 表示读写。缺了 intent,编译器必须保守处理,还可能放行你无意的修改。
subroutine scale_vec(v, factor) use iso_fortran_env, only: real64 implicit none real(real64), intent(inout) :: v(:) ! 读改写 real(real64), intent(in) :: factor ! 只读 v = v * factor end subroutine scale_vec
默认参数传递是传址(by reference),零拷贝但会互相干扰。value 属性强制传值:在栈上建副本,彻底隔离。互操作时(第 7 章的 C 调用)或需要防"被改"的标量参数,用 value。大数组别用 value——拷贝成本吞掉一切收益。

pure 过程承诺无副作用:不修改全局状态、不读写文件、不改变非 intent 参数。这个承诺让编译器放心地在多线程与 GPU 上并行调用它。elemental 更进一步:定义对标量的操作,编译器允许把它直接应用到数组,自动生成 SIMD 循环。
module physics_mod use iso_fortran_env, only: real64 implicit none contains ! 元素级纯函数:对标量定义,可整体作用于数组 pure real(real64) function air_density(t) result(rho) real(real64), intent(in) :: t rho = 1.225_real64 * (288.15_real64 / (t + 273.15_real64)) end function air_density elemental real(real64) function e_density(t) result(rho) real(real64), intent(in) :: t rho = 1.225_real64 * (288.15_real64 / (t + 273.15_real64)) end function e_density end module physics_mod program elemental_demo use physics_mod, only: e_density use iso_fortran_env, only: real64 implicit none real(real64) :: temps(4) temps = [0.0_real64, 10.0_real64, 20.0_real64, 30.0_real64] write(*,*) e_density(temps) ! 数组实参,自动逐元素计算 end program elemental_demo
elemental 的代价:参数必须全标量,过程内不能有依赖数组形状的逻辑。识别计算内核的并行特征,把它们封装成 pure/elemental 过程,是让代码"天生可并行"的关键动作——第 6 章 OpenMP 并行循环里能放心调用的,正是这类过程。
现代 Fortran 支持递归,但要用 recursive 关键字声明,且要明白每次调用都在栈上分配新的局部变量。深度递归会栈溢出;编译器也难以对递归做内联。控制手段:先估递归深度,超出栈预算就改写迭代;真正的尾递归部分编译器可能优化成循环,但别依赖。树遍历、分形这类自然递归的算法用它没问题,海量数据的线性处理用迭代。
同一算法要同时支持单精度、双精度、复数时,F77 风格是 sgemm、dgemm、zgemm 一串名字。泛型接口把多个具体过程绑到一个通用名下,编译器按实参类型在编译期分派——调用者只记一个名字。
module generic_mod use iso_fortran_env, only: real32, real64 implicit none interface normalize module procedure normalize_r32, normalize_r64 end interface normalize contains pure function normalize_r32(v) result(w) real(real32), intent(in) :: v(:) real(real32) :: w(size(v)) w = v / maxval(abs(v)) end function normalize_r32 pure function normalize_r64(v) result(w) real(real64), intent(in) :: v(:) real(real64) :: w(size(v)) w = v / maxval(abs(v)) end function normalize_r64 end module generic_mod program use_generic use generic_mod, only: normalize use iso_fortran_env, only: real32, real64 implicit none real(real32) :: a(3) real(real64) :: b(3) a = [3.0_real32, 1.0_real32, 2.0_real32] b = [3.0_real64, 1.0_real64, 2.0_real64] write(*,*) normalize(a) ! 自动选单精度版本 write(*,*) normalize(b) ! 自动选双精度版本 end program use_generic
这是编译期多态,零运行时开销。陷阱:两个具体过程签名要足够不同,否则实参能同时匹配两个时,编译器报歧义错误。运算符重载(+ 作用于自定义类型)与赋值重载(assignment(=))同理,都是把"同名多实现"扩展到运算符。注意克制:只在语义直观、符合数学惯例的场景重载,否则代码会变得晦涩。
阅读完本节,你应当能够:
过程会写了,下一步是把它挂到类型上——面向对象登场。