-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprocedure.tex
More file actions
1283 lines (1227 loc) · 58.6 KB
/
procedure.tex
File metadata and controls
1283 lines (1227 loc) · 58.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\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