\chapter{过程} \label{fortran_procedure} 

假如我们需要用 Fortran 算阶乘 $ 10! $, 那还是很容易滴. 
\begin{lstlisting} 
program main
    implicit none
    integer :: i, p
    p = 1
    do i = 1, 10
        p = p * i
    end do
    print *, p
end program main
\end{lstlisting} 

假如我们需要用 Fortran 算组合数 $ \text{C} _7^3=\frac{7!} {3!(7-3)!} $, 那就有点麻烦. 
\begin{lstlisting} 
program main
    implicit none
    integer :: i, c1, c2, c3, c
    c1 = 1
    do i = 1, 7
        c1 = c1 * i
    end do
    c2 = 1
    do i = 1, 3
        c2 = c2 * i
    end do
    c3 = 1
    do i = 1, 7-3
        c3 = c3 * i
    end do
    c = c1 / (c2*c3)
    print *, c
end program main
\end{lstlisting} 
麻烦的地方在于那个阶乘老是要 \ttt{do} 来 \ttt{do} 去, 不过就 \ttt{do} 三回, 还能活. 

假如我们需要用 Fortran 算 CG 系数 $ \left\langle 3, 2;5, 4|7, 6\right\rangle  $, 
\begin{align*} 
    \left\langle j_1, m_1;j_2, m_2|j_3, m_3\right\rangle&=\delta_{m_3, m_1+m_2} \Big[(2j_3+1)\\
    &\cdot\frac{(j_1+j_2-j_3)!(j_2+j_3-j_1)!(j_3+j_1-j_2)!} {(j_1+j_2+j_3+1)!} 
    \\
    &\cdot\prod_{i=1, 2, 3}(j_i+m_i)!(j_i-m_i)!\Big]^{1/2} \sum_{\nu\in F} [(-1)^{\nu} \nu!\\
    &\cdot(j_1+j_2-j_3-\nu)!\\
    &\cdot(j_1-m_1-\nu)!(j_2+m_2-\nu)!\\
    &\cdot(j_3-j_1-m_2+\nu)!(j_3-j_2+m_1+\nu)!], 
\end{align*} 
那不知要 \ttt{do} 多少回, 算个大头鬼哟! 不算了, 准备卸 Fortran 了!

ちょっと待って, Fortran 是有法子能偷懒滴 (如果没有我第一个卸 Fortran), 比如算 $ \text{C} _7^3 $ 可以这样. 
\begin{lstlisting} 
program main
    implicit none
    integer :: c, factorial
    c = factorial(7) &
        / (factorial(3)*factorial(7-3))
    print *, c
end program main
\end{lstlisting}
\begin{lstlisting}
function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    integer :: i 
    p = 1
    do i = 1, n
        p = p * i
    end do
end function factorial
\end{lstlisting} 
写成这样, 确实能少打几个字儿. 不知道我写的是什么也可以先猜猜看. 把 \ttt{function ...} 到 \ttt{end function ...} 单看成一个程序, \ttt{result(p)} 里的 \ttt{p} 就是 \ttt{factorial(n)} 里的 \ttt{n} 的阶乘, 如是这般, \ttt{factorial(7)} 就是 \ttt{7} 的阶乘, \ttt{factorial(3)} 就是 \ttt{3} 的阶乘, \ttt{factorial(7-3)} 就是 \ttt{7-3} 的阶乘, 非常完美! 那算 CG 系数也简单多了 (虽然还是很复杂), 只要算 \ttt{X} 的阶乘的时候无脑写上 \ttt{factorial(X)} 就成了, 不用 \ttt{do} 来 \ttt{do} 去了!

于是乎我们便发现, 想要玩 Fortran 而不是被 Fortran 玩, 就必须懂过程 (procedure). 因为一个 ``重新创造轮子'' 的梗 (请同志们自行搜索了解), 过程又俗称轮子 (wheel). 过程的定义是 ``封装可以在程序执行期间直接调用的任意操作序列的实体'', 玄玄乎乎的, 我们不理它. 我们可以直接把过程理解成程序运行时的一个操作, 比如上面的例子中 \ttt{function ...} 到 \ttt{end function ...} 就是 ``计算 \ttt{n} 的阶乘'' 这一操作. 使用过程后, 我们就进入面向过程程序设计 (procedure-oriented programming, POP) 阶段了.

啥叫面向过程呢? 众所周知, 置象于冰箱中, 步骤有三: 一开冰箱, 二塞大象, 三关冰箱. 用 Fortran 来写便这般. 
\begin{lstlisting}[numbers=none] 
program main
    ... 
    implicit none
    ... 
    call open_door()
    call put_in(elephant)
    call close_door()
end program main
\end{lstlisting}
\begin{lstlisting}[numbers=none]
subroutine open_door()
    ... 
end subroutine open_door
\end{lstlisting}
\begin{lstlisting}[numbers=none]
subroutine put_in(what_put_in)
    ... 
end subroutine put_in
\end{lstlisting}
\begin{lstlisting}[numbers=none]
subroutine close_door()
    ... 
end subroutine close_door
\end{lstlisting} 
这是个典型的面向过程的程序, 程序把自己要干的事分成几步骤, 每个步骤都单造一个过程表示. 这样一看主程序就知道这程序干仨事儿: 第一步 \ttt{call open\_{}door()} 开冰箱, 第二步 \ttt{call put\_{}in(elephant)} 塞大象, 第三步 \ttt{call close\_{}door()} 关冰箱, 非常清楚. 至于具体咋么开的冰箱, 咋么塞的大象, 咋么关的冰箱, 看对应的 \ttt{subroutine} 到 \ttt{end subroutine} 里咋写的便能知晓. 把整个程序 (冰箱里塞大象) 拆成一个个步骤 (开冰箱, 塞大象, 关冰箱), 然后每个步骤造个过程, 这就是面向过程. 那为什么塞大象要写成 \ttt{put\_{}in(elephant)} 而不是 \ttt{put\_{}elephant\_{}in()}? 这是因为说不准以后要把别的东西放进冰箱, 真有那天, 简单地把 \ttt{elephant} 换成别的东西就行了, 哦, 还要把大象拿出来, 再多造个名称为 \ttt{put\_{}out} 的过程, 然后写 \ttt{call put\_{}out(elephant)} 就行了.

\section{外部过程} 

我们先细掰一些基本概念. 任何过程都是要用一堆字符表示的, 这堆字符便称为子程序(subprogram). 过程和子程序的关系就像程序和源代码的关系一样. 子程序按摆的位置, 分为外部子程序 (external subprogram), 内部子程序 (internal subprogram) 和模块子程序 (module subprogram). 内部子程序瞅着没啥用还容易让同志们脑壳疼, 俺不打算讲, 模块子程序见第\ref{fortran_module}章, 本章只讲外部子程序. 子程序按长的样子, 又分为子例行子程序 (subroutine subprogram) 和函数子程序 (function subprogram). 

\subsection{子例行子程序} 

我们来详细分析下面这个程序. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: a, b, c
    real(dp) :: x, y, z
    a = 1.0_dp
    b = 2.0_dp
    c = 3.0_dp
    x = 4.0_dp
    y = 5.0_dp
    z = 6.0_dp
    call ab2bc_then_sumabc(x, y, z)
    print *, a, b, c
    print *, x, y, z
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine ab2bc_then_sumabc(a, b, c)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: a
    real(dp), intent(inout) :: b
    real(dp), intent(out) :: c
    real(dp) :: s
    c = b
    b = a
    s = a + b + c
    print *, s
end subroutine ab2bc_then_sumabc
\end{lstlisting} 
\begin{enumerate} 
    \item 从 \ttt{subroutine ...} 到 \ttt{end subroutine ...} 便是子例行子程序了. 这部分可以和主程序放在一个文件里, 顺序也随便, 但通常是单独放在另一个文件里. 
    \item \ttt{subroutine} 和 \ttt{end subroutine} 后的 \ttt{XX} 称为子例行子程序名, 例如示例中 \ttt{XX} 为 \ttt{ab2bc\_{}then\_{}sumabc}. 一般存这个子程序的文件就会取成 \ttt{XX.f90}, 例如示例中的子程序可以存入名为 \ttt{ab2bc\_{}then\_{}sumabc.f90} 的文件中.
    \item 子程序和主程序一样都是程序单元 (见 \ref{program_unit} 节), 一样得变量声明一波, 所以 \ttt{implicit none} 得加, 要用种别的话 \ttt{use iso\_{}fortran\_{}env} 也得加. 
    \item 子程序里有三个变量 \ttt{a}, \ttt{b}, \ttt{c}, 我在声明时加了 \ttt{, intent(...)}, 使这三个变量有了 intent 属性. 这三个变量在 \ttt{ab2bc\_{}then\_{}sumabc} 后的 \ttt{()} 里出现, 在 \ttt{()} 里出现的称为哑参量 (dummy argument), 只有哑参量能在声明时加 \ttt{, intent(...)}. 我称加 \ttt{, intent(in)} 的为只读(read-only)参量, 加 \ttt{, intent(inout)} 的为读写(read-write)参量, 加 \ttt{, intent(out)} 的为只写(write-only)参量. 
    \item 主程序里也有三个变量 \ttt{a}, \ttt{b}, \ttt{c}, 但只是变量名和子程序里的 \ttt{a}, \ttt{b}, \ttt{c} 一样, 实际上是不同的三个变量. 看我这示例, 子程序里 \ttt{a}, \ttt{b}, \ttt{c} 变来变去, 花里胡哨, 主程序里 \ttt{a}, \ttt{b}, \ttt{c} 岿然不动. 
    \item 主程序里出现 \ttt{call ab2bc\_{}then\_{}sumabc(x, y, z)}, \ttt{()} 里的 \ttt{x}, \ttt{y}, \ttt{z} 称为实参量 (actual argument). 实参量可以是任意数据实体, 也就是说实参量可以是变量, 也可以是常量或其他东东. 子程序里三个哑参量排排坐, 主程序里三个实参量排排坐, 位置一样的哑参量和实参量 (\ttt{a} 和 \ttt{x}, \ttt{b} 和 \ttt{y}, \ttt{c} 和 \ttt{z}) 称为对应的. 对应的哑参量和实参量在程序运行时会相互赋值来赋值去, 这称为参量结合 (argument association), 我们一般称为哑实结合. 
    \item 现在分析示例程序的运行过程. 程序当然从 \ttt{program main} 开始运行了. 按顺序一行一行运行, 前面不需讲解. 到 \ttt{call ...}, 就要说道说道了. 我们可以把主程序和子程序当成两个小人儿. \begin{enumerate} 
        \item \ttt{call ab2bc\_{}then\_{}sumabc(x, y, z)}: 跳到子程序的开头, 也就是 \ttt{subroutine ab2bc\_{}then\_{}sumabc(a, b, c)}. \ttt{call ...} 这里程序运行的操作称为 ``主程序调用 (invoke/call) 子程序'' . 
        \item \ttt{subroutine ab2bc\_{}then\_{}sumabc(a, b, c)}: 子程序先按后面的变量声明语句声明好变量, 然后把所有只读的和读写的实参量赋值给对应的哑参量 (主程序里的 \ttt{x} 赋给子程序里的 \ttt{a}, 主程序里的 \ttt{y} 赋给子程序里的 \ttt{b}, 这当然就是传说中的哑实结合啦). 
        \item 向下一行一行运行, 直到 \ttt{end subroutine ab2bc\_{}then\_{}sumabc}. 啰嗦一下具体过程. 首先主程序里的 \ttt{x} 赋给子程序里的 \ttt{a}, 主程序里的 \ttt{y} 赋给子程序里的 \ttt{b}, 所以子程序里的 \ttt{a} 为 \ttt{4.0\_{}dp}, 子程序里的 \ttt{b} 为 \ttt{5.0\_{}dp}. 然后 \ttt{c = b}, 子程序里的 \ttt{c} 为 \ttt{5.0\_{}dp}, 然后 \ttt{b = a}, 子程序里的 \ttt{b} 为 \ttt{4.0\_{}dp}, 然后 \ttt{s = a + b + c}, \ttt{s} 为 \ttt{13.0\_{}dp}, 最后输出 \ttt{s} 的值 \ttt{13.0\_{}dp}. 
        \item \ttt{end subroutine ab2bc\_{}then\_{}sumabc}: 把所有读写的和只写的哑参量赋值给对应的实参量 (子程序里的 \ttt{b} 赋给主程序里的 \ttt{y}, 子程序里的 \ttt{c} 赋给主程序里的 \ttt{z}, 这当然也是传说中的哑实结合啦), 然后跳到 \ttt{call ab2bc\_{}then\_{}sumabc(x, y, z)} 的下一行. 
    \end{enumerate} 然后主程序继续按顺序一行一行运行至 \ttt{end program main} 结束. 再啰嗦啰嗦, 子程序里的 \ttt{b} 赋给主程序里的 \ttt{y}, 子程序里的 \ttt{c} 赋给主程序里的 \ttt{z}, 所以 \ttt{x} 还是 \ttt{4.0\_{}dp}, \ttt{y} 则变为 \ttt{4.0\_{}dp}, \ttt{z} 则变为 \ttt{5.0\_{}dp}. 
\end{enumerate} 

啊! 上面那个程序终于分析完毕! 不仅是主程序能调用子程序, 任何程序单元都能调用子程序, 所以我们还可以玩点更花的. 假如我们现在要算组合数 $ \text{C} _7^3 $, 我们可以造一个主程序, 一个算组合数的子程序, 一个算阶乘的子程序, 然后让主程序调用算组合数的子程序, 算组合数的子程序调用算阶乘的子程序, 就像下面这样. 请同志们自己分析其运行过程. \label{fact_comb} 
\begin{lstlisting} 
program main
    implicit none
    integer :: result
    call combinatorial(7, 3, result)
    print *, result
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine combinatorial(n, m, comb)
    implicit none
    integer, intent(in) :: n
    integer, intent(in) :: m
    integer, intent(out) :: comb
    integer :: a, b, c
    call factorial(n, a)
    call factorial(m, b)
    call factorial(n-m, c)
    comb = a / (b*c)
end subroutine combinatorial
\end{lstlisting}
\begin{lstlisting}
subroutine factorial(n, fact)
    implicit none
    integer, intent(in) :: n
    integer, intent(out) :: fact
    integer :: i
    fact = 1
    do i = 1, n
        fact = fact * i
    end do
end subroutine factorial
\end{lstlisting} 

子程序灰常管用, 但也是要遵守一些禁令的. 首先哑实结合时, 哑参量和实参量的类型和种别\uline{都要相等}, 也就是说莫得类型种别转化了. 比如下面这个程序, Gfortran 日常严格, 会直接报错, Ifx 日常宽松, 不出警告, 但输出的竟然是 \ttt{10.00000}\dots{}
\begin{lstlisting} 
program main
    use iso_fortran_env, only: sp => real32
    implicit none
    real(sp) :: a
    a = 10.0_sp
    call add_one(a)
    print *, a
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine add_one(a)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(inout) :: a
    a = a + 1.0_dp
end subroutine add_one
\end{lstlisting} 
然后只读的 (加 \ttt{, intent(in)} 的) 哑参量不能在子程序运行的时候被赋值 (哑实结合时当然还是可以的), 比如下面这个程序跑不得, Ifx 和 Gfortran 都是如此. 这个规则是非常适当的, 因为如果我们可以确定一个哑参量不应当在子程序运行的时候被赋值, 我们就可以让这个哑参量成为只读的, 这样如果我们一不小心写错了, 在子程序运行的时候给这个哑参量赋值了, 编译器就能在编译时马上查出来, 免得我们乱跑程序跑了很久结果还不对. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: a
    a = 10.0_dp
    call add_one(a)
    print *, a
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine add_one(a)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: a
    a = a + 1.0_dp
end subroutine add_one
\end{lstlisting} 
注意间接的赋值也是不行的, 比如下面这个程序, \ttt{call add\_{}one\_{}(a)} 实际上给 \ttt{add\_{}one} 里的 \ttt{a} 赋值了. 但这样 ``隐晦的'' 赋值, 编译器就不一定会查了, Gfortran 会报错, 而 Ifx 会直接放行. 但无论如何这么写都是不对的!\label{secret_assignment} 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: a
    a = 10.0_dp
    call add_one(a)
    print *, a
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine add_one(a)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: a
    call add_one_(a)
end subroutine add_one
\end{lstlisting}
\begin{lstlisting}
subroutine add_one_(a)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(inout) :: a
    a = a + 1.0_dp
end subroutine add_one_
\end{lstlisting} 
还有只写的 (加 \ttt{, intent(out)} 的) 哑参量, 在子程序运行的一开始都是未定义的, 不论与哑参量结合的实参量是否未定义. 所以下面这程序中, 即使主程序里的 \ttt{a} 不是未定义的, 子程序 \ttt{add\_{}one} 的第6行赋值时 ``\ttt{=}'' 右边的 \ttt{a} 也是未定义的, 这个程序老天也不知会出什么结果. 但 Ifx 和 Gfortran 有器规, 会把只写参量当成读写参量 (我猜是这样了啦), 一开始也实参量赋值给哑参量了, 所以结果没事. 但无论如何这么写都是不对的!
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: a
    a = 10.0_dp
    call add_one(a)
    print *, a
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine add_one(a)
    ! Dummy argument may be undefined here!
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(out) :: a
    a = a + 1.0_dp
end subroutine add_one
\end{lstlisting} 

正确标注哑参量为只读参量, 只写参量或读写参量\label{arguments} (加 \ttt{intent(...)}), 是灰常灰常必要的, 血泪教训告诉我们这么做能避免踩很多很多坑. 同志们一定不能怕麻烦, 老老实实一个个标注. 如果哑参量除哑实结合时外不被赋值, 就标只读, 如果哑参量在哑实结合时不需要被赋值, 就标只写, 剩下的标读写. 
\begin{convention} 
    正确标注每个哑参量为只读参量, 只写参量或读写参量. 
\end{convention} 

子程序还有很多禁令, 请同志们自己写程序测试, 编译器会告诉大家答案的. 比如, 子程序名能和主程序名一样吗? 子程序名能和子程序里的变量的变量名一样吗? 子程序名能和调用子程序的程序单元里的变量的变量名一样吗? 子程序能直接或间接地调用自己吗?\dots{}

\subsection{函数子程序} 

如果同志们没被子例行子程序弄晕, 那理解函数子程序便轻而易举了. 我们还是写一个计算组合数的子例行子程序, 不过我用 \ttt{...} 省略一部分, 想来同志们自己补上没问题. 
\begin{lstlisting} 
program main
    implicit none
    integer :: result
    call combinatorial(7, 3, result)
    print *, result
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine combinatorial(n, m, comb)
    implicit none
    integer, intent(in) :: n
    integer, intent(in) :: m
    integer, intent(out) :: comb
    integer :: a, b, c
    integer :: i
    ... 
    comb = a / (b*c)
end subroutine combinatorial
\end{lstlisting} 
上面这个程序完全可以用函数子程序改写成下面这样, 请同志们对比改写前后的样子. 
\begin{lstlisting} 
program main
    implicit none
    integer :: combinatorial
    integer :: result
    result = combinatorial(7, 3)
    print *, result
end program main
\end{lstlisting}
\begin{lstlisting}
function combinatorial(n, m) result(comb)
    implicit none
    integer, intent(in) :: n
    integer, intent(in) :: m
    integer :: comb
    integer :: a, b, c
    integer :: i
    ... 
    comb = a / (b*c)
end function combinatorial
\end{lstlisting} 
\begin{enumerate} 
    \item 从 \ttt{function ...} 到 \ttt{end function ...} 便是函数子程序了. 和子例行子程序一样, 可以和主程序放在一个文件里, 顺序也随便, 但通常是单独放在另一个文件里.  \ttt{function} 和 \ttt{end function} 后的 \ttt{XX} (示例中为 \ttt{combinatorial}) 一样称为函数子程序名. 一般存这个子程序的文件也一样会取成 \ttt{XX.f90}. 
    \item 函数子程序名后的 \ttt{()} 里的当然是哑参量, \ttt{()} 后的 \ttt{result(...)} 里的变量 \ttt{...} (示例中为 \ttt{comb}) 相当于一个只写哑参量, 称为结果(result), 但结果本身不是哑参量. 声明结果时不需也不能加 \ttt{, intent(out)}. 
    \item 调用函数子程序后, 结果赋值给函数名和其之后的括号及括号内的内容组成的整体. 示例中, 一开始主程序里 \ttt{7} 和 \ttt{3} 赋值给子程序里 \ttt{n} 和 \ttt{m}, 最后子程序里结果 \ttt{comb} 赋值给主程序里 \ttt{combinatorial(7, 3)} 这一整个长串, \ttt{combinatorial(7, 3)} 就成为一个数据实体, 因此可以再赋值给主程序里的 \ttt{result} 变量. 
    \item 调用函数子程序前必须对函数子程序本身进行声明 (声明的类型和种别是函数子程序的结果的类型和种别), 子例行子程序是不需要的. 比如示例程序的主程序加了 \ttt{integer :: combinatorial} 一句, 因为整型是函数 \ttt{combinatorial} 的结果 \ttt{comb} 的类型. 
\end{enumerate} 
按照当今的规范, 我们必须保证函数子程序的所有哑参量都是只读的 (结果不是哑参量). 如果不遵守此规范, 我保证同志们之后会无比头痛. 
\begin{convention}\label{func_all_in}
    标注函数子程序的所有哑参量为只读参量. 
\end{convention} 

使用函数子程序的好处是调用函数子程序后会生成一个数据实体, 经验表明多数情况下这样能让我们偷懒少打几个字, 即便使用函数子程序前必须多加一行函数子程序的声明. 我把之前第 \pageref{fact_comb} 页用计算阶乘的子程序计算组合数的程序用函数子程序改写如下, 同志们会不会觉得看着简单一点? 
\begin{lstlisting} 
program main
    implicit none
    integer :: combinatorial
    print *, combinatorial(7, 3)
end program main
\end{lstlisting}
\begin{lstlisting}
function combinatorial(n, m) result(comb)
    implicit none
    integer, intent(in) :: n
    integer, intent(in) :: m
    integer :: comb
    integer :: factorial
    comb = factorial(n) &
           / (factorial(m)*factorial(n-m))
end function combinatorial
\end{lstlisting}
\begin{lstlisting}
function factorial(n) result(fact)
    implicit none
    integer, intent(in) :: n
    integer :: fact
    integer :: i
    fact = 1
    do i = 1, n
        fact = fact * i
    end do
end function factorial
\end{lstlisting} 

\subsection{固有过程} 

\newcommand{\ip} [1]{\href{https://fortranwiki.org/fortran/show/#1} { \ttt{#1} } } 
为了让我们能快乐地玩轮子, 合格的 Fortran 编译器都已经自己造好了一大堆轮子, 称为固有过程 (intrinsic procedure), 我们直接调用就可以了. 同志们可猛看\href{https://j3-fortran.org/doc/year/24/24-007.pdf}{标准解释文档}第 355 页表 16.1 或猛戳\href{https://fortranwiki.org/fortran/show/Intrinsic+procedures}{这个链接}查询固有轮子有哪些怎么用, 没必要全背. 同志们造轮子前都应该先查查有没有已经造好的固有轮子可以用. 比如我们如果想算 $ \pi $, 如果我们很熟悉固有轮子的话, 我们就会想到有个轮子 \ttt{acos}, 是算反余弦的, 我们用它算 $ \arccos(-1) $ 即可. 另外固有过程都不需要声明, 即使固有过程是函数.
\begin{lstlisting} 
program main
    use iso_fortran_env, only: qp => real128
    implicit none
    print *, acos(-1.0_qp)
end program main
\end{lstlisting} 
请同学们自己想办法用固有过程算 $e$.

\section{过程中的变量}

\subsection{save 属性} 

下面这个程序, 连续输出三个 \ttt{1}, 这当然没有问题. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    call count()
    call count()
    call count()
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine count()
    implicit none
    integer :: n
    n = 0
    n = n + 1
    print *, n
end subroutine count
\end{lstlisting} 
但下面这个程序输出的却是 \ttt{1}, \ttt{2}, \ttt{3}. 在这个程序里, 子程序 \ttt{count} 里的 \ttt{n} 声明的时候加了 \ttt{, save}, 并赋值 \ttt{0}, 这使 \ttt{n} 有了 save 属性, 成为已保存变量 (saved variable), 相当于 C 的静态局域变量 (static local variable). 第一次调用 \ttt{count} 的时候, \ttt{n} 一开始是 \ttt{0}, 然后 \ttt{n = n + 1}, \ttt{n} 就是 \ttt{1}. 而第二次调用 \ttt{count} 的时候, \ttt{n} 一开始并没有重新被赋值成 \ttt{0}, 而是保存着上一次调用到最后的值 \ttt{1}, 所以再次 \ttt{n = n + 1} 后 \ttt{n} 变成 \ttt{2}. 第三次调用 \ttt{count} 的时候, \ttt{n} 一开始是 \ttt{2}, 所以最后是 \ttt{3}. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    call count() ! 0 -> 1
    call count() ! 1 -> 2
    call count() ! 2 -> 3
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine count()
    implicit none
    integer, save :: n = 0
    n = n + 1
    print *, n
end subroutine count
\end{lstlisting} 
有的同志可能会尝试在变量声明的时候直接给变量初始化, 因为这样可以偷一点懒, 但这么做是非常危险的, 因为这么做的时候, 即使没加 \ttt{, save}, 变量也悄咪咪地带上 save 属性了, 这是 Fortran 又一个经典的坑. 比如下面这个程序和上面那个程序是一样的, 但因为没写 \ttt{, save}, 同志们可能就会忘记变量 \ttt{n} 有 save 属性!
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    call count()
    call count()
    call count()
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine count()
    implicit none
    ! n has SAVE attribute!!!
    integer :: n = 0 
    n = n + 1
    print *, n
end subroutine count
\end{lstlisting} 
因此, 不要这么写, 除非声明时标有 \ttt{, parameter} 或 \ttt{, save}. 
\begin{convention} 
    除非已经明确标明变量有 parameter 属性或 save 属性, 否则不得在声明变量的时候直接给变量初始化. 
\end{convention} 

\subsection{过程中的字符串} 

在用子程序的时候碰上字符串, 就会比较麻烦, 因为哑实结合还和字符串的长度有关. 我们可以直接把字符串哑参量的长度写成 \ttt{*}, 这样字符串哑参量的长度会自动确定成与它结合的字符串实参量的长度, 非常方便. 下面的程序中, 子程序的 \ttt{char\_{}in} 的长度会自动确定成主程序的 \ttt{char\_{}in} 的长度 \ttt{3}, 子程序的 \ttt{char\_{}out} 的长度会自动确定成主程序的 \ttt{char\_{}out} 的长度 \ttt{1}.
\begin{lstlisting}
program main
    implicit none
    character(3), parameter :: char_in = 'Hi!'
    character(1) :: char_out
    print *, char_in
    call to_screen(char_in, char_out)
    print *, char_out
end program main
\end{lstlisting}
\begin{lstlisting}
subroutine to_screen(char_in, char_out)
    implicit none
    character(*), intent(in) :: char_in
    character(*), intent(out) :: char_out
    print *, char_in, len(char_in)
    char_out = char_in
    print *, char_out, len(char_out)
end subroutine to_screen
\end{lstlisting}

\subsection{过程中的数组} 

在用子程序的时候碰上数组, 就会比较麻烦, 因为哑实结合还和数组的形状有关. 我们必须细掰细掰. 

\subsubsection{显式形状数组} 

一个 $ n $ 维向量 $ r=(r_1, \dots, r_n) $ 的 $ 1 $ 范数为 $ \left\lVert r\right\rVert_1:=\sum_{i=1} ^n\left\lvert r_i\right\rvert  $ . 假如我们要算 $(-1, 2) $ 的 $ 1 $ 范数, 写个轮子还是很容易滴. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: r(2)
    real(dp) :: norm_1
    r = [-1.0_dp, 2.0_dp]
    print *, norm_1(r)
end program main
\end{lstlisting}
\begin{lstlisting}
function norm_1(r) result(norm)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: r(2)
    real(dp) :: norm
    norm = sum(abs(r))
end function
\end{lstlisting} 
其中主程序里的 \ttt{r} 和函数里的 \ttt{r}, 形状都大大咧咧的写明在那里, 这就叫显式形状数组 (explicit-shape array). 但我们上面这个程序大有问题, 如果我们要算 $(-1, 2, -3) $ 的 $ 1 $ 范数, 因为函数里的 \ttt{r} 形状被定死为 \ttt{[2]}, 所以要出事儿. 这时我们可以这么写. \label{adjustable_array} 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: r_2(2), r_3(3)
    real(dp) :: norm_1
    r_2 = [-1.0_dp, 2.0_dp]
    r_3 = [-1.0_dp, 2.0_dp, -3.0_dp]
    print *, norm_1(r_2, size(r_2))
    print *, norm_1(r_3, size(r_3))
end program main
\end{lstlisting}
\begin{lstlisting}
function norm_1(r, size_r) result(norm)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: r(size_r)
    integer, intent(in) :: size_r
    real(dp) :: norm
    norm = sum(abs(r))
end function
\end{lstlisting} 
这个程序, 函数里哑参量 \ttt{r} 的形状是由哑参量 \ttt{size\_{}r} 决定的, 这样的哑参量数组就叫可调数组 (adjustable array), 定义为显式形状数组中的一种. 虽然 \ttt{size\_{}r} 的声明在 \ttt{r} 下面, 但放心, 声明 \ttt{r} 的时候会先查看 \ttt{size\_{}r} 的值的. 

第 \pageref{hw_2} 页的小作业二是计算一个奇怪矩阵的所有元素的和, 我们可以把问题扩大点, 算任意 $ n\times n $ 的那种矩阵的所有元素的和, 我们则可以这么写. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: strange_mat_element_sum
    print *, strange_mat_element_sum(50)
end program main
\end{lstlisting}
\begin{lstlisting}
function strange_mat_element_sum(n) result(s)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: s
    real(dp) :: mat(n, n)
    integer :: i, j
    do i = 1, n
        do j = 1, n
            mat(i, j) = sqrt(real(i+j-1))
        end do
    end do
    s = sum(mat)
end function
\end{lstlisting} 
这个程序, 函数里 \ttt{mat} 的形状是由哑参量 \ttt{n} 决定的, 这样的数组就叫自动数组(automatic array), 也定义为显式形状数组中的一种. 自动数组和可调数组的区别是可调数组一定是哑参量, 自动数组一定不是哑参量. 

\subsubsection{假定形状数组} \label{assumed-shape} 

之前第 \pageref{adjustable_array} 页用可调数组的轮子, 每次都要算数组的大小, 然后和子程序哑实结合, 还是麻烦. 用假定形状数组 (assumed-shape array) 就可以这么解决这个问题. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: r_2(2), r_3(3)
    real(dp) :: norm_1
    r_2 = [-1.0_dp, 2.0_dp]
    r_3 = [-1.0_dp, 2.0_dp, -3.0_dp]
    print *, norm_1(r_2)
    print *, norm_1(r_3)
end program main
\end{lstlisting}
\begin{lstlisting}
function norm_1(r) result(norm)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: r(:)
    real(dp) :: norm
    norm = sum(abs(r))
end function
\end{lstlisting} 
在这个程序里, 函数中 \ttt{r} 就是假定形状数组, 其声明的时候写 \ttt{r(:)}, 1 个 \ttt{:} 表示 \ttt{r} 必须是 1 维数组, 形状是其对应的实参量的形状, 这样就不用每次都计算实参量的大小然后哑实结合了. 不过上面这个轮子是跑不了的, 即使编译器允许跑, 结果也很可能是错的, 因为按 Fortran 的语法, 有假定形状数组哑参量的过程, 必须带过程接口 (见 \ref{fortran_interface} 节), 所以要写成下面这个样子才行.\label{assumed-shape_array_program} 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: r_2(2), r_3(3)
    interface
        function norm_1(r) result(norm)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: r(:)
            real(dp) :: norm
        end function
    end interface
    r_2 = [-1.0_dp, 2.0_dp]
    r_3 = [-1.0_dp, 2.0_dp, -3.0_dp]
    print *, norm_1(r_2)
    print *, norm_1(r_3)
end program main
\end{lstlisting}
\begin{lstlisting}
function norm_1(r) result(norm)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: r(:)
    real(dp) :: norm
    norm = sum(abs(r))
end function
\end{lstlisting} 
使用假定形状数组的时候, 我们还可以指定假定形状数组每一维的下界. 比如下面这个程序, 函数里的 \ttt{r} 是2维数组, 第1维下界是0, 第2维下界没写, 默认是1. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: r_2(2, 1), r_3(3, 1)
    interface
        function norm_1(r) result(norm)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: r(0:,:)
            real(dp) :: norm
        end function
    end interface
    r_2 = reshape([0.0_dp, -1.0_dp], [2, 1])
    r_3 = reshape([0.0_dp, -1.0_dp, 2.0_dp], [3, 1])
    print *, norm_1(r_2)
    print *, norm_1(r_3)
end program main
\end{lstlisting}
\begin{lstlisting}
function norm_1(r) result(norm)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: r(0:,:)
    real(dp) :: norm
    norm = sum(abs(r))
end function
\end{lstlisting} 

\subsection{哑过程} 

有的时候我们需要把子程序本身当成参量, 比如我们如果要造个定积分的轮子, 我们就要被积函数, 下界, 上界三个参量, 被积函数参量当然得是函数啦. 是哑参量的过程称为哑过程 (dummy procedure). 下面就是个定积分轮子, 虽然看着非常复杂, 但确实能跑. 这个轮子将在 \ref{fortran_interface} 节中讲解. \label{dummy_procedure_program} 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function identity(x) result(y)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: x
            real(dp) :: y
        end function
    end interface
    real(dp) :: integrate
    print *, integrate(identity, 0.0_dp, 1.0_dp)
end program main
\end{lstlisting}
\begin{lstlisting}
function integrate(f, a, b) result(s)
    ! Use trapezoidal rule.
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function f(x) result(y)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: x
            real(dp) :: y
        end function
    end interface
    real(dp), intent(in) :: a
    real(dp), intent(in) :: b
    real(dp) :: s
    real(dp) :: h
    integer :: i
    h = (b-a) / 10000
    s = (f(a)+f(b)) / 2
    do i = 1, 9999
        s = s + f(a+i*h)
    end do
    s = s * h
end function integrate
\end{lstlisting}
\begin{lstlisting}
function identity(x) result(y)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: x
    real(dp) :: y
    y = x
end function identity
\end{lstlisting} 

\section{特殊过程}

\subsection{纯过程}\label{pure_procedure}

按规范 \ref{func_all_in} 写出的函数都是纯 (pure) 过程. 纯过程的定义有点复杂, 但同学们只需知道最典型的纯过程, 就是过程的哑参量 (函数结果不算) 都是只读参量的过程. 写纯过程子程序时, 在开头的 \ttt{subroutine} 或 \ttt{function} 前加 \ttt{pure}, 示例如下.
\begin{lstlisting}
program main
    implicit none
    integer :: factorial
    print *, factorial(3)
end program main
\end{lstlisting}
\begin{lstlisting}
pure function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    integer :: i 
    p = 1
    do i = 1, n
        p = p * i
    end do
end function factorial
\end{lstlisting}
给哑参量加 intent 属性能帮我们避坑, 造纯过程能进一步帮我们避坑, 因为如果我们可以确定一个过程的哑参量都是只读参量, 我们就可以让这个过程成为纯过程, 这样如果我们一不小心写错了, 让哑参量不是只读参量了, 编译器就能在编译时马上查出来, 免得我们乱跑程序跑了很久结果还不对. 

\subsection{逐元过程}\label{elemental_procedure}

如果我们使用固有函数 \ttt{log}, 我们会发现它非常神奇, 它可以接收任意形状的数组, 并把数组中的所有元素分别都求自然对数, 形成形状相同的新数组. 其实很多常用的固有函数都如此.
\begin{lstlisting}
program main
    implicit none
    integer :: i
    print *, log(real(1))
    print *, log([(real(i), i = 1, 9)])
end program main
\end{lstlisting}
这是因为 \ttt{log} 是逐元 (elemental) 过程. 逐元过程就是可以接收任意形状的数组, 并把数组中的所有元素分别都进行操作, 形成形状相同的新数组的过程. 逐元过程必须是纯过程. 我们可以把 \ref{pure_procedure} 小节的示例中的纯过程子程序中 \ttt{pure} 改成 \ttt{elemental}, 将纯过程子程序改写成逐元过程子程序, 注意 \ttt{elemental} 已表明过程是纯过程, 我们没必要再写 \ttt{pure}.
\begin{lstlisting}
program main
    implicit none
    integer :: i, factorial
    print *, factorial(3)
    print *, factorial([(i, i = 1, 9)])
end program main
\end{lstlisting}
\begin{lstlisting}
elemental function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    integer :: i 
    p = 1
    do i = 1, n
        p = p * i
    end do
end function factorial
\end{lstlisting}
不过上面这个轮子是跑不了的, 即使编译器允许跑, 结果也很可能是错的, 因为按 Fortran 的语法, 被使用的逐元过程, 必须带过程接口 (见 \ref{fortran_interface} 节), 所以要写成下面这个样子才行.\label{elemental_procedure_program} 
\begin{lstlisting}
program main
    implicit none
    integer :: i
    interface
        elemental function factorial(n) result(p)
            integer, intent(in) :: n
            integer :: p
        end function factorial
    end interface
    print *, factorial(3)
    print *, factorial([(i, i = 1, 9)])
end program main
\end{lstlisting}
\begin{lstlisting}
elemental function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    integer :: i 
    p = 1
    do i = 1, n
        p = p * i
    end do
end function factorial
\end{lstlisting}

\subsection{递归过程}

因为 $n!=n\cdot(n-1)!$, 所以我们可以玩点花活儿. 我们可以尝试把 \ref{pure_procedure} 小节的示例改写成下面这样, 其中函数 \ttt{factorial} 调用自己, 表示 $n!=n\cdot(n-1)!$. 但下面这个轮子是跑不了的, 因为 Fortran 不允许一个普通的过程调用自己.
\begin{lstlisting}
program main
    implicit none
    integer :: factorial
    print *, factorial(3)
end program main
\end{lstlisting}
\begin{lstlisting}
function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    if (n == 1) then
        p = 1
    else
        ! factorial(n) == n * factorial(n-1)
        p = n * factorial(n-1)
    end if
end function factorial
\end{lstlisting}
我们可以在子程序开头的 \ttt{subroutine} 或 \ttt{function} 前加 \ttt{recursive}, 将过程改造成递归 (recursive) 过程. 递归过程就是可以调用自己的过程. 于是花活儿就像下面这样玩成了.
\begin{lstlisting}
program main
    implicit none
    integer :: factorial
    print *, factorial(3)
end program main
\end{lstlisting}
\begin{lstlisting}
recursive function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    if (n == 1) then
        p = 1
    else
        ! factorial(n) == n * factorial(n-1)
        p = n * factorial(n-1)
    end if
end function factorial
\end{lstlisting}

不过上面这个例子我们还要细掰细掰. 同学们可能会这么分析上面这个例子.
\begin{enumerate}
    \item 主程序第4行要算 \ttt{factorial(3)}, 所以函数 \ttt{factorial} 的哑元 \ttt{n} 变成 \ttt{3}.
    \item \ttt{n /= 1}, 所以 \ttt{p = n * factorial(n-1)}, 要算 \ttt{factorial(n-1)}, 所以函数 \ttt{factorial} 的哑元 \ttt{n} 变成 \ttt{n-1}, 所以 \ttt{n} 变成 \ttt{2}.
    \item \ttt{n /= 1}, 所以 \ttt{p = n * factorial(n-1)}, 要算 \ttt{factorial(n-1)}, 所以函数 \ttt{factorial} 的哑元 \ttt{n} 变成 \ttt{n-1}, 所以 \ttt{n} 变成 \ttt{1}.
    \item \ttt{n == 1}, 所以 \ttt{p = 1}, \ttt{p} 变成 \ttt{1}, 然后到 \ttt{end function factorial}, \ttt{p} 是结果, 所以 \ttt{factorial(3)} 是 \ttt{1}.
\end{enumerate}
可这明显不对呀, 程序跑出来结果应该是 \ttt{6} 才对嘛. 要想弄懂上面这个例子, 我们需要更准确地理解过程的调用. 过程调用时, 严格意义上说, 不是过程本身被调用了, 而是过程连同过程中的所有常量变量都被复制出分身, 然后分身被调用了. 所以上面这个例子应分析如下.
\begin{enumerate}
    \item 主程序第4行要算 \ttt{factorial(3)}, 所以函数 \ttt{factorial} 被复制出分身 \ttt{factorial}$_1$, 哑元 \ttt{n}$_1$ 变成 \ttt{3}.
    \item \ttt{n}$_1$ \ttt{/= 1}, 所以 \ttt{p}$_1$ \ttt{= n}$_1$ \ttt{* factorial(n}$_1$\ttt{-1)}, 要算 \ttt{factorial(n}$_1$\ttt{-1)}, 所以函数 \ttt{factorial} 被复制出分身 \ttt{factorial}$_2$, \ttt{n}$_2$ \ttt{= n}$_1$\ttt{-1}, 哑元 \ttt{n}$_2$ 变成 \ttt{2}.
    \item \ttt{n}$_2$ \ttt{/= 1}, 所以 \ttt{p}$_2$ \ttt{= n}$_2$ \ttt{* factorial(n}$_2$\ttt{-1)}, 要算 \ttt{factorial(n}$_2$\ttt{-1)}, 所以函数 \ttt{factorial} 被复制出分身 \ttt{factorial}$_3$, \ttt{n}$_3$ \ttt{= n}$_2$\ttt{-1}, 哑元 \ttt{n}$_3$ 变成 \ttt{1}.
    \item \ttt{n}$_3$ \ttt{== 1}, 所以 \ttt{p}$_3$ \ttt{= 1}, \ttt{p}$_3$ 变成 \ttt{1}, 然后到 \ttt{end function factorial}, \ttt{p}$_3$ 是结果, 而 \ttt{p}$_2$ \ttt{= n}$_2$ \ttt{* factorial(n}$_2$\ttt{-1)} 时调用 \ttt{factorial}$_3$, 所以 \ttt{factorial(n}$_2$\ttt{-1)} 是 \ttt{1}.
    \item \ttt{n}$_2$ 是 \ttt{2}, 所以 \ttt{p}$_2$ 变成 \ttt{2}, 然后到 \ttt{end function factorial}, \ttt{p}$_2$ 是结果, 而 \ttt{p}$_1$ \ttt{= n}$_1$ \ttt{* factorial(n}$_1$\ttt{-1)} 时调用 \ttt{factorial}$_2$, 所以 \ttt{factorial (n}$_1$\ttt{-1)} 是 \ttt{2}.
    \item \ttt{n}$_1$ 是 \ttt{3}, 所以 \ttt{p}$_1$ 变成 \ttt{6}, 然后到 \ttt{end function factorial}, \ttt{p}$_1$ 是结果, 而主程序算 \ttt{factorial(3)} 时调用 \ttt{factorial}$_1$, 所以 \ttt{factorial(3)} 是 \ttt{6}.
\end{enumerate}
啊! 这么分析就正确了!

如果想让过程又是纯过程又是递归过程, 我们把 \ttt{pure} 和 \ttt{recursive} 都加在子程序开头的 \ttt{subroutine} 或 \ttt{function} 前即可 (\ttt{pure} 和 \ttt{recursive} 顺序任意), 像下面这样.
\begin{lstlisting}
program main
    implicit none
    integer :: factorial
    print *, factorial(3)
end program main
\end{lstlisting}
\begin{lstlisting}
recursive pure function factorial(n) result(p)
    integer, intent(in) :: n
    integer :: p
    if (n == 1) then
        p = 1
    else
        ! factorial(n) == n * factorial(n-1)
        p = n * factorial(n-1)
    end if
end function factorial
\end{lstlisting}
想让过程又是逐元过程又是递归过程亦是同理, 请同学们自己尝试.

如果能理解透递归过程的运作原理, 在可以使用递归过程的时候用递归过程, 程序是会很简洁很秀的. 但是如果想让程序跑得快, 递归过程最好不用. 因为程序每调用一个过程, 都会在内存中专门为这个过程分配一块存储空间, 术语称作进栈 (push stack), 而用递归过程时, 过程可能会被调用灰常多次 (例如 \ttt{factorial(1000)} 会调用 \ttt{factorial} $1000$ 次), 最后的结果是递归过程被多多地调用, 内存被多多地占着, 电脑不堪重负, 程序跑得慢甚至崩溃出错.

\section{过程接口} \label{fortran_interface} 

之前第 \pageref{assumed-shape_array_program} 页, 第 \pageref{dummy_procedure_program} 页, 第 \pageref{elemental_procedure_program} 页的轮子用了过程接口 (procedure interface). 过程接口可分为三类: 特定接口 (specific interface), 泛型接口 (generic interface) 和抽象接口 (abstract interface). 

\subsection{特定接口} 

特定接口相当于过程的声明. 通常情况下, 子例行不需要声明, 函数也只需要声明结果就可以了. 但如果碰上了以下情形之一, 那么就一定要加特定接口\footnote{一定要加特定接口的情形还有很多, 其他同志们暂时不需要掌握.}:\label{whether_specific_interface}
\begin{itemize} 
    \item 过程有一个哑参量, 此哑参量满足下列条件之一: \begin{itemize} 
        \item 是延迟长度字符型变量 (见 \ref{fortran_char} 节) 或延迟形状数组(见\ref{fortran_array_specification} 节);
        \item 是假定形状数组;
    \end{itemize} 
    \item 过程是函数且结果是数组;
    \item 过程是哑过程;
    \item 过程是逐元过程.
\end{itemize} 
这不一定需要背, 编译器应该是要告诉我们的. 我们可以先不加接口, 然后如果编译器告诉我们 ``Explicit interface required for \dots{}'' 或 ``Expected a procedure for argument \dots{}'' 或其他七七八八的话, 那就是要加接口了. 

特定接口都放在从 \ttt{interface} 到 \ttt{end interface} 的一整块里, 这一整块称为接口块 (interface block), 一个接口块里可以放一堆接口. 接口是过程的一部分, 我们只要把过程的变量声明部分下面的执行部分都删掉, 然后变量声明部分, 除了哑参量和结果的声明, 其他声明都删掉, 然后复制粘贴到接口块里就可以了. 

我们通过实战来熟悉这个过程. 假设我们现在想搞个算单位矩阵的轮子, 我们正常地这么一写, 结果跑不得. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: i(3, 3)
    i = eye(3)
end program main
\end{lstlisting}
\begin{lstlisting}
function eye(n) result(mat)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: mat(n, n)
    !----------------------------------------
    integer :: i
    mat = 0.0_dp
    do i = 1, n
        mat(i, i) = 1.0_dp
    end do
    !----------------------------------------
end function eye
\end{lstlisting} 
我们需要在主程序中加个接口. 我们先在主程序的声明部分写上 \ttt{interface} 和 \ttt{end interface}, 然后把整个 \ttt{eye} 子程序复制粘贴进去. (我们还可以用列选择\footnote{不知道什么是列选择的同志请自行搜索了解.}来调整格式)
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function eye(n) result(mat)
            use iso_fortran_env, only: dp => real64
            implicit none
            integer, intent(in) :: n
            real(dp) :: mat(n, n)
            !----------------------------------------
            integer :: i
            mat = 0.0_dp
            do i = 1, n
                mat(i, i) = 1.0_dp
            end do
            !----------------------------------------
        end function eye
    end interface
    real(dp) :: i(3, 3)
    i = eye(3)
end program main
\end{lstlisting}
\begin{lstlisting}
function eye(n) result(mat)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: mat(n, n)
    !----------------------------------------
    integer :: i
    mat = 0.0_dp
    do i = 1, n
        mat(i, i) = 1.0_dp
    end do
    !----------------------------------------
end function eye
\end{lstlisting} 
然后我们定睛一看, 两个注释行中间的部分, 第1行是 \ttt{i} 的声明, \ttt{i} 既不是哑参量也不是结果, 所以删去, 后面几行是执行部分也删去. 删完以后就成下面这个样子. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function eye(n) result(mat)
            use iso_fortran_env, only: dp => real64
            implicit none
            integer, intent(in) :: n
            real(dp) :: mat(n, n)
        end function eye
    end interface
    real(dp) :: i(3, 3)
    i = eye(3)
end program main
\end{lstlisting}
\begin{lstlisting}
function eye(n) result(mat)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: mat(n, n)
    !----------------------------------------
    integer :: i
    mat = 0.0_dp
    do i = 1, n
        mat(i, i) = 1.0_dp
    end do
    !----------------------------------------
end function eye
\end{lstlisting} 
然后这个轮子就能跑了, 欧耶!

同志们会不会觉得这么做挺麻烦的? 俺也觉得, 可是这也是没办法的. Fortran 在设置这个接口规则的时候可是经过深思熟虑的, 因为造编译器的那些人, 凭他们的经验告诉我们, 有些情况 (比如第 \pageref{whether_specific_interface} 页列出来的), 如果没有接口, 编译器很容易编译错程序, 然后出大事情. 不过还是有办法能让我们少费点脑子, 那就是使用模块 (见第\ref{fortran_module}章), 但如果不需要加接口, 那么造一个模块反而比较费事\dots{}

这里还有个问题, 如果我们碰到个固有过程需要接口怎么办, 比如我们如果要算 $ \sin $ 的积分, 我们用固有过程 \ttt{sin} 直接干算不得.
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: integrate
    print *, integrate(sin, 0.0_dp, 1.0_dp)
end program main
\end{lstlisting}
\begin{lstlisting}
function integrate(f, a, b) result(s)
    ! Use trapezoidal rule.
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function f(x) result(y)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: x
            real(dp) :: y
        end function
    end interface
    real(dp), intent(in) :: a
    real(dp), intent(in) :: b
    real(dp) :: s
    real(dp) :: h
    integer :: i
    h = (b-a) / 10000
    s = (f(a)+f(b)) / 2
    do i = 1, 9999
        s = s + f(a+i*h)
    end do
    s = s * h
end function integrate
\end{lstlisting} 
最无脑的办法就是我们再造个过程, 把固有过程变成外部过程, 像下面这样, 然后再加上接口即可. 请同志们自己给下面这个程序补上接口. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: integrate
    print *, integrate(sin_, 0.0_dp, 1.0_dp)
end program main
\end{lstlisting}
\begin{lstlisting}
function integrate(f, a, b) result(s)
    ! Use trapezoidal rule.
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function f(x) result(y)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: x
            real(dp) :: y
        end function
    end interface
    real(dp), intent(in) :: a
    real(dp), intent(in) :: b
    real(dp) :: s
    real(dp) :: h
    integer :: i
    h = (b-a) / 10000
    s = (f(a)+f(b)) / 2
    do i = 1, 9999
        s = s + f(a+i*h)
    end do
    s = s * h
end function integrate
\end{lstlisting}
\begin{lstlisting}
function sin_(x) result(y)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: x
    real(dp) :: y
    y = sin(x)
end function sin_
\end{lstlisting} 

\subsection{泛型接口} 

泛型接口是一个比较妙的东东. 假如我们现在要造个算符号函数 $ \sgn $ 的轮子, 那是轻而易举的. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp) :: sgn
    print *, sgn(10.0_dp)
end program main
\end{lstlisting}
\begin{lstlisting}
function sgn(x) result(y)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: x
    real(dp) :: y
    if (x > 0.0_dp) then
        y = 1.0_dp
    else if (x < 0.0_dp) then
        y = -1.0_dp
    else
        y = 0.0_dp
    end if
end function sgn
\end{lstlisting} 
但有个问题, 就是假如如果我们要算四精度, 只能另造个轮子. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64, &
                               qp => real128
    implicit none
    real(dp) :: sgn_real64
    real(qp) :: sgn_real128
    print *, sgn_real64(10.0_dp)
    print *, sgn_real128(10.0_qp)
end program main
\end{lstlisting}
\begin{lstlisting}
function sgn_real64(x) result(y)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: x
    real(dp) :: y
    if (x > 0.0_dp) then
        y = 1.0_dp
    else if (x < 0.0_dp) then
        y = -1.0_dp
    else
        y = 0.0_dp
    end if
end function sgn_real64
\end{lstlisting}
\begin{lstlisting}
function sgn_real128(x) result(y)
    use iso_fortran_env, only: qp => real128
    implicit none
    real(qp), intent(in) :: x
    real(qp) :: y
    if (x > 0.0_qp) then
        y = 1.0_qp
    else if (x < 0.0_qp) then
        y = -1.0_qp
    else
        y = 0.0_qp
    end if
end function sgn_real128
\end{lstlisting} 
这就让我们有点小小的不开心, Fortran 的语法也太严格了! 我们希望能造个 \ttt{sgn}, 又能算双精度又能算四精度. 用泛型接口, 我们就可以偷鸡摸狗地 ``做成'' 这件事, 把上面的 \ttt{sgn\_{}real64} 和 \ttt{sgn\_{}real128}  ``粘起来'' . 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64, &
                               qp => real128
    implicit none
    interface sgn
        function sgn_real64(x) result(y)
            use iso_fortran_env, only: dp => real64
            implicit none
            real(dp), intent(in) :: x
            real(dp) :: y
        end function sgn_real64
        function sgn_real128(x) result(y)
            use iso_fortran_env, only: qp => real128
            implicit none
            real(qp), intent(in) :: x
            real(qp) :: y
        end function sgn_real128
    end interface
    print *, sgn(10.0_dp)
    print *, sgn(10.0_qp)
end program main
\end{lstlisting}
\begin{lstlisting}
function sgn_real64(x) result(y)
    use iso_fortran_env, only: dp => real64
    implicit none
    real(dp), intent(in) :: x
    real(dp) :: y
    if (x > 0.0_dp) then
        y = 1.0_dp
    else if (x < 0.0_dp) then
        y = -1.0_dp
    else
        y = 0.0_dp
    end if
end function sgn_real64
\end{lstlisting}
\begin{lstlisting}
function sgn_real128(x) result(y)
    use iso_fortran_env, only: qp => real128
    implicit none
    real(qp), intent(in) :: x
    real(qp) :: y
    if (x > 0.0_qp) then
        y = 1.0_qp
    else if (x < 0.0_qp) then
        y = -1.0_qp
    else
        y = 0.0_qp
    end if
end function sgn_real128
\end{lstlisting} 
我们会发现泛型接口和特定接口很像, 只不过 \ttt{interface} 后多了一串 (示例中为 \ttt{sgn}), 然后 \ttt{sgn\_{}real64} 和 \ttt{sgn\_{}real128} 的接口都写在接口块里. 这么写后, 电脑看到 \ttt{sgn(10.0\_{}dp)}, 就会发现 \ttt{10.0\_{}dp} 是双精度的, 然后电脑就会在标 \ttt{sgn} 的泛型接口里找, 看 \ttt{sgn\_{}real64} 的接口, 发现 \ttt{sgn\_{}real64} 这个函数参量是双精度的, 匹配, 又看 \ttt{sgn\_{}real64} 的接口, 发现 \ttt{sgn\_{}real128} 这个函数参量是四精度的, 不匹配, 于是乎电脑就会自动把 \ttt{sgn(10.0\_{}dp)} 里的 \ttt{sgn} 当成 \ttt{sgn\_{}real64} 了. 然后电脑看到 \ttt{sgn(10.0\_{}qp)}, 也是一样, 只不过最后是把 \ttt{sgn} 当成 \ttt{sgn\_{}real128}. 这样就仿佛有一个又能算双精度又能算四精度的 \ttt{sgn} 了!

Fortran 的类型--种别--维数匹配 (type-kind-rank compatibility, TKR compatibility) 超严格的, 但我们用泛型接口便能有所突破, 这当然是大好事. 但同志们还是可能不开心, 因为同志们会发现上面的 \ttt{sgn\_{}real64} 和 \ttt{sgn\_{}real128} 其实长得基本上是一模一样, 就是变量种别不同, 如果我们还要 \ttt{sgn} 能接收单精度参量, 能接收整型参量, 岂不是要复制粘贴写一大堆一模一样的过程, 读起来还脑壳疼? 非常遗憾, 据我的了解, Fortran 自己确实就只能这么玩儿了. 不过用第\ref{fortran_preprocessor}章介绍的预处理器的话, 就可以省事儿不少还易读 (我用过, 超好用), 但这已经是超 Fortran 的内容了. 

\subsection{抽象接口} 

特定接口只对应一个过程, 而抽象接口则对应所有特征 (characteristic) 相同的过程, 我们来看下面这个程序. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    interface
        function eye(n) result(mat)
            use iso_fortran_env, only: dp => real64
            implicit none
            integer, intent(in) :: n
            real(dp) :: mat(0:n-1, 0:n-1)
        end function eye
        function minkowski(n) result(eta)
            use iso_fortran_env, only: dp => real64
            implicit none
            integer, intent(in) :: n
            real(dp) :: eta(0:n-1, 0:n-1)
        end function minkowski
    end interface
    real(dp), dimension(0:3, 0:3) :: mat_i, mat_m
    mat_i = eye(4)
    mat_m = minkowski(4)
end program main
\end{lstlisting}
\begin{lstlisting}
function eye(n) result(mat)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: mat(0:n-1, 0:n-1)
    integer :: i
    mat = 0.0_dp
    do i = 0, n-1
        mat(i, i) = 1.0_dp
    end do
end function eye
\end{lstlisting}
\begin{lstlisting}
function minkowski(n) result(eta)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: eta(0:n-1, 0:n-1)
    integer :: i
    eta = 0.0_dp
    eta(0, 0) = -1.0_dp
    do i = 1, n-1
        eta(i, i) = 1.0_dp
    end do
end function minkowski
\end{lstlisting} 
这个程序, 接口块里写了两个过程的接口, 这当然没有问题. 但同志们会发现这两个过程其实接口长得 ``一模一样'', 都是函数, 都只有一个整型只读参量, 结果的类型, 结果的种别和结果的形状也一样, 只不过各种名称 (过程名, 参量名, 结果名) 不一样而已. 接口长得 ``一模一样'' 的过程, 我们称为特征相同的. 能不能趁机偷一小点懒? 能, 像下面这样. 
\begin{lstlisting} 
program main
    use iso_fortran_env, only: dp => real64
    implicit none
    abstract interface
        function dim2mat(dim) result(mat)
            use iso_fortran_env, only: dp => real64
            implicit none
            integer, intent(in) :: dim
            real(dp) :: mat(0:dim-1, 0:dim-1)
        end function dim2mat
    end interface
    procedure(dim2mat) :: eye, minkowski
    real(dp), dimension(0:3, 0:3) :: mat_i, mat_m
    mat_i = eye(4)
    mat_m = minkowski(4)
end program main
\end{lstlisting}
\begin{lstlisting}
function eye(n) result(mat)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: mat(0:n-1, 0:n-1)
    integer :: i
    mat = 0.0_dp
    do i = 0, n-1
        mat(i, i) = 1.0_dp
    end do
end function eye
\end{lstlisting}
\begin{lstlisting}
function minkowski(n) result(eta)
    use iso_fortran_env, only: dp => real64
    implicit none
    integer, intent(in) :: n
    real(dp) :: eta(0:n-1, 0:n-1)
    integer :: i
    eta = 0.0_dp
    eta(0, 0) = -1.0_dp
    do i = 1, n-1
        eta(i, i) = 1.0_dp
    end do
end function minkowski
\end{lstlisting} 
和特定接口相比, 抽象接口的 \ttt{interface} 前多了个 \ttt{abstract}. 接口块里过程名 \ttt{dim2mat} 的接口和过程 \ttt{eye}, \ttt{minkowski} 的特定接口长得 ``一模一样'': 都是函数, 都只有一个整型只读参量等等. 至于抽象接口里函数名是 \ttt{dim2mat}, 参量名是 \ttt{dim}, 结果名是 \ttt{mat}, 无关紧要, 这些名称都是可以随便取的\footnote{如果碰上了什么规矩, 编译器会告诉我们的!}, 只需保证抽象接口的特征和本需加特定接口的过程的特征相同即可. 然后我们只需要再写 \ttt{procedure(dim2mat) :: eye, minkowski} 就相当于给和 \ttt{dim2mat} 特征相同的过程 \ttt{...} (示例中是 \ttt{eye} 和 \ttt{minkowski}) 加特定接口了.
