-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtraits.tex
More file actions
2944 lines (2602 loc) · 141 KB
/
traits.tex
File metadata and controls
2944 lines (2602 loc) · 141 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
\documentclass[11pt,oneside]{report}
\usepackage[a4paper, total={6.5in, 9in}]{geometry}
\usepackage{url}
\usepackage{listings}
\usepackage{listings-rust}
\usepackage[scaled=0.82]{beramono}
\usepackage[T1]{fontenc}
\usepackage{hyperref}
\usepackage{xcolor}
\usepackage[title]{appendix}
\usepackage{mathpazo}
%\usepackage{mathptmx}
\usepackage{array}
\usepackage{booktabs}
\usepackage{tablefootnote}
\usepackage[autostyle]{csquotes}
\usepackage[
backend=biber,
bibstyle=numeric,
citestyle=numeric,
sorting=nyt,
sortcites=true,
abbreviate=false,
defernumbers=true,
]{biblatex}
\defbibheading{bibempty}{}
\addbibresource{traits.bib}
\newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
\newcolumntype{C}[1]{>{\centering\arraybackslash}p{#1}}
\newcommand{\code}[1]{{\selectfont\ttfamily{#1}}}
\lstdefinelanguage{LFortran}[]{Fortran}{
morekeywords={abstract,import,class,type,typeof,typedef,
deferred,pass,nopass,static,extends,implements,sealed,
associate,generic,initial,init,itself,real32,real64}
}
\lstdefinelanguage{Swift6}[]{Swift}{
morekeywords={any}
}
\hypersetup{
colorlinks,
linkcolor={blue!70!black},
citecolor={blue!70!black},
urlcolor={blue!70!black}
}
\frenchspacing
\begin{document}
\title{\textbf{Traits, Generics, and Modern-day OOP for Fortran}}
\author{Konstantinos Kifonidis, Ondrej Certik, Derick Carnazzola}
\maketitle
\abstract{Based on conclusions drawn from a survey of modern
languages, a traits system for Fortran is developed that allows for
the uniform expression of run-time and compile-time polymorphism in
the language, and thus for the uniform management of source code
dependencies on user-defined and language-intrinsic types. The
feature set that is described here is small enough to facilitate a
first prototype implementation in an open source compiler (like
LFortran, LLVM Flang, or GNU Fortran), but at the same time
comprehensive enough to already endow Fortran with polymorphic
capabilities that widely equal those of modern programming languages like
Swift, Rust, Go, Carbon, or Mojo. The discussed extensions are fully
backwards compatible with present Fortran, and enable modern-day,
traits-based, object-oriented programming, and powerful, easy to
use, fully type-checked, generics. The latter support seamlessly
both the procedural, functional, and object-oriented programming
styles, and they largely get by \emph{without} requiring manual
instantiations by the user. The presented design could also be
naturally extended to support rank-genericity for arrays, structural
subtyping, and compile-time polymorphic union/sum types. Its modern
capabilities are expected to transform the way both Fortran
applications and libraries will be written in the future. Decoupled
software plugin architectures with enormously improved source code
flexibility, reusability, maintainability, and reliability will
become possible, without any need for run-time type inspections, and
without any loss in computational performance.}
\chapter{Introduction}
Polymorphism was discovered in the 1960ies by Kristen Nygaard and
Ole-Johan Dahl during their development of Simula~67, the world's
first object-oriented (OO) language \cite{Dahl_04}. Their work
introduced into programming what is nowadays known as ``virtual method
table'' (i.e. function-pointer) based run-time polymorphism, which is
both the first focus of this document, and the decisive feature of all
OO languages. Several other forms of polymorphism are known today, the
most important of them being parametric polymorphism
\cite{Cardelli_Wegner_85}, also known as ``generics'', which is the
second focus of this document, and which has historically developed
disjointly from run-time polymorphism since it makes use of
compile-time mechanisms.
\section{The purpose of polymorphism}
But what is the purpose of polymorphism in a programming language?
What is polymorphism actually good for? One of the more comprehensive
answers to this question was given by Robert C. Martin in numerous
books (e.g. \cite{Martin_17}), as well as in the following quotation
from his blog \cite{Martin_14}:
\begin{displayquote}
``There really is only one benefit to polymorphism; but it's a big
one. It is the inversion of source code and run time dependencies.
In most software systems when one function calls another, the
runtime dependency and the source code dependency point in the same
direction. The calling module depends on the called module. However,
when polymorphism is injected between the two there is an inversion
of the source code dependency. The calling module still depends on
the called module at run time. However, the source code of the
calling module does not depend upon the source code of the called
module. Rather both modules depend upon a polymorphic
interface.
This inversion allows the called module to act like a
plugin. Indeed, this is how all plugins work.''
\end{displayquote}
Notice the absence of the words ``code reuse'' in these statements.
The purpose of polymorphism, according to Martin, is the ``inversion''
(i.e. replacement, or management) of rigid source code dependencies by
means of particular abstractions, i.e. polymorphic interfaces (or
proto\-cols/traits, as they are also known today). The possibility to
reuse code is then merely the logical consequence of such proper
dependency management in a polymorphism-based software plugin
architecture.
\section{Source code dependencies in statically typed languages}
Which then are the source code dependencies that polymorphism helps us
manage? It has been customary to make the following distinction when
answering this question:
\begin{itemize}
\item
Firstly, most larger programs that are written in statically typed
languages (like Fortran) have dependencies on user-defined
procedures and data types. If the programmer employs encapsulation
of both a program's procedures and its data, i.e. its state, both
these dependencies can actually be viewed as dependencies on
\emph{user-defined abstract data types}. These are the dependencies
that Martin is concretely referring to in the above quotation, and
it is these dependencies on (volatile) user-defined data and
implementations that are particularly troublesome, because they lead
to rigid coupling between the various different \emph{parts} of an
application. Their results are recompilation cascades, the
non-reusability of higher-level source code units, the impossibility
to comprehend a large application incrementally, and fragility of
such an application as a whole.
\item
Secondly, every program, that is written in a statically typed
language, also has dependencies on abstract data types that are
provided by the language itself. Fortran's \code{integer},
\code{real}, \code{complex}, etc., intrinsic types are examples of
\emph{language-intrinsic abstract data types}. While hard-wired
dependencies on such intrinsic types do not couple different parts
of a program (because the implementations of these types are
supplied by the language), they nevertheless make a program's source
code rigid with respect to the data that it can be used on.
\end{itemize}
The most widely used approaches to manage dependencies on
language-intrinsic types have so far been through generics, while
dependency management of user-defined (abstract data) types has so far
been the task of OO programming and OO design patterns. Martin
\cite{Martin_17} has, for instance, defined object-orientation as
follows:
\begin{displayquote}
``OO is the ability, through the use of polymorphism, to gain
absolute control over every source code dependency in [a software]
system. It allows the architect to create a plugin architecture, in
which modules that contain high-level policies are independent of
modules that contain low-level details. The low-level details are
relegated to plugin modules that can be deployed and developed
independently from the modules that contain high-level policies.''
\end{displayquote}
\section{Modern developments}
Notice how Martin's modern definition of object-orientation, that
emphasizes source code decoupling, is the antithesis to the usually
taught ``OO'' approaches of one class rigidly inheriting
implementation code from another. Notice also how his definition does
not require some specific type of polymorphism for the task of
dependency management, as long as (according to Martin's first
quotation) the mechanism is based on polymorphic interfaces.
Martin's statements on the purpose of both polymorphism and OO simply
reflect the two crucial developments that have taken place in these
fields over the last decades. Namely, the realizations that
\begin{itemize}
\item
run-time polymorphism should be freed from the conflicting concept
of implementation inheritance (to which it was originally bound
given its Simula~67 heritage), and be formulated exclusively in
terms of conformance to polymorphic interfaces, i.e. function
signatures, or purely procedural abstractions, and that
\item
compile-time polymorphism should be formulated in exactly the same
way as well.
\end{itemize}
These two developments, taken together, have recently opened up the
possibility to treat polymorphism, and hence the dependency management
of both user-defined and language-intrinsic types, uniformly in a
programming language. As a consequence, it has become possible to use
the potentially more efficient (but also less flexible) mechanism of
compile-time polymorphism also for certain tasks that have
traditionally been reserved for run-time polymorphism (in the form of
OO programming), and to mix and match the two polymorphism types
inside a single application to better satisfy a user's needs for both
flexibility and efficiency.
\section{Historical background}
The road towards these realizations was surprisingly long. Over
the last five decades, a huge body of OO programming experience first
had to demonstrate that the use of (both single and multiple)
implementation inheritance breaks encapsulation in OO languages, and
therefore results in extremely tightly coupled, rigid, fragile, and
non-reusable code. This led to an entire specialized literature on OO
design patterns \cite{Gamma_et_al_94,Martin_03,Holub_04}, that aimed
at avoiding such rigidity by replacing the use of implementation
inheritance with the means to formulate run-time polymorphism that are
discussed below. It also led to the apprehension that implementation
inheritance (but \emph{not} run-time polymorphism) should be abandoned
\cite{Weck_Szyperski}. In modern languages, implementation inheritance
is either kept solely for backwards compatibility reasons (e.g. in the
Swift and Carbon languages), or it is foregone altogether (e.g. in
Rust, and Go).
The first statically typed mainstream programming language that
offered a proper separation of run-time polymorphism from
implementation inheritance was Objective-C. It introduced
``protocols'' (i.e. polymorphic interfaces) in the year 1990
\cite{Cox_et_al_20}. Protocols in Objective-C consist of pure function
signatures, that lack implementation code. Objective-C provided a
mechanism to implement multiple such protocols by a class, and to thus
make classes conform to protocols. This can be viewed as a restricted
form of multiple inheritance, namely inheritance of object
\emph{specification}, which is also known as \emph{subtyping}.
Only a few years later, in 1995, the Java language hugely popularized
these ideas using the terms ``interfaces'' and ``interface
inheritance'' \cite{Cox_et_al_20}. Today, nearly all modern languages
support polymorphic interfaces/protocols, and the basic mechanism of
multiple interface inheritance that was introduced to express run-time
polymorphism in Objective-C, often in even improved, more flexible,
manifestations. The only negative exceptions in this respect being
modern Fortran, and C++, which both still stick to the obsolescent
Simula~67 paradigm.
A similarly lengthy learning process, as that outlined for run-time
polymorphism, also took place in the field of compile-time/parametric
polymorphism. Early attempts, notably templates in C++, to render
function arguments and class parameters polymorphic, did not impose
any constraints on such arguments and parameters, that could be
checked by C++ compilers. With the known results on compilation times
and cryptic compiler error messages
\cite{Haveraaen_et_al_19}. Surprisingly, Java, the language that truly
popularized polymorphic interfaces in OO programming, did not provide
an interface based mechanism to constrain its generics. Within the
pool of mainstream programming languages, this latter realization was
only made with the advent of Rust \cite{Matsakis_2014}.
Rust came with a traits (i.e. polymorphic interfaces) system with which
it is possible for the user to uniformly and transparently express
both generics (i.e. compile-time) and run-time polymorphism in the
same application, and to relatively easily switch between the two,
where possible. Rust's traits are an improved form of
protocols/interfaces in that the user can implement them for a type
without having these implementations be coupled to the type's actual
definition. Thus, existing types can be made to retroactively
implement new traits, and hence be used in new settings (with some
minor restrictions on user ownership of either the traits or the
types).
Rust's main idea was quickly absorbed by almost all other mainstream
modern languages, most notably Swift, Go, and Carbon, with the
difference that these latter languages tend to leave the choice
between static and dynamic binding of procedures to the compiler, or
language implementation, rather than the programmer. C++ is in the
process of adopting generics constraints for its ``templates'' under
the term ``strong concepts'', but without implementing the greater
idea to uniformly express \emph{all} the polymorphism in the language
through traits. An implementation of this latter idea must today be
viewed as a prerequisite in order to call a language design
``modern''. The purpose of this document is to describe extensions to
Fortran, that aim to provide the Fortran language with such modern
capabilities.
\chapter{Case study: Calculating the average value of a numeric array}
To illustrate the advanced features and capabilities of some of the
available modern programming languages with respect to polymorphism,
and hence dependency management, we will make use here of a case
study: the simple test problem of calculating the average value of a
set of numbers that is stored inside a one-dimensional array. In the
remainder of this chapter, we will first provide an account and some
straightforward monomorphic (i.e. rigidly coupled) functional
implementation of this test problem, followed by a functional
implementation that makes use of both run-time and compile-time
polymorphism to manage rigid source code dependencies. In the survey
of programming languages that is presented in
Chapter~\ref{chapt:survey}, we will then recode this standard test
problem in an encapsulated fashion, to highlight how the source code
dependencies in this problem can be managed in different languages
even in more complex situations, that require OO techniques.
\section{Monomorphic functional implementation}
\label{sect:mono_functional}
We have chosen Go here as a language to illustrate the basic ideas.
Go is easily understood, even by beginners, and is therefore well
suited for this purpose (another good choice would have been the Swift
language). The code in the following Listing~\ref{lst:funcGo} should
be self explanatory for anyone who is even only remotely familiar with
the syntax of C family languages. So, we'll make only a few remarks
regarding syntax.
\begin{itemize}
\item
While mostly following a C like syntax, variable declarations in Go
are essentially imitating Pascal syntax, where a variable's name
precedes the declaration of the type.
\item
Go has two assignment operators. The usual \code{=} operator, as it
is known from other languages, and the separate \code{:=} operator,
that employs automatic type inference for combined declaration and
initialization of a variable.
\item
Go has array slices that most closely resemble those of Python's
Numpy (which exclude the upper bound of an array slice).
\end{itemize}
Our basic algorithm for calculating the average value of an array of
integer elements employs two different implementations for
averaging. The first makes use of a ``simple'' summation of all the
array's elements, in ascending order of their array index. While the
second sums in a ``pairwise'' manner, dividing the array in half to
carry out the summations recursively, and switching to the ``simple''
method once subdivision is no longer possible. In both cases, the
resulting sum is then divided by the array's number of elements, to
obtain the desired average.
\lstinputlisting[language=Go,style=boxed,label={lst:funcGo},caption={Monomorphic functional version of the array averaging example in Go.}]{Code/Go/coupled.go}
An inspection of Listing~\ref{lst:funcGo} will readily reveal that
this code has three levels of rigid (i.e. hard-wired)
dependencies. Namely,
\begin{enumerate}
\item
function \code{pairwise\_sum} depending on function
\code{simple\_sum}'s implementation,
\item
functions \code{simple\_average} and \code{pairwise\_average}
depending on functions' \code{simple\_sum} and \code{pairwise\_sum}
implementations, respectively, and
\item
the entire program depending rigidly on the \code{int32} data type in
order to declare both the arrays that it is operating on, and
the results of its summation and averaging operations.
\end{enumerate}
The first two items are dependencies on user-defined implementations,
while the third is a typical case of rigid dependency on a
language-intrinsic type, which renders the present code incapable of
being applied to arrays of any other data type than
\code{int32}s. Given that we are dealing with three levels of
dependencies, three levels of polymorphism will accordingly be
required to remove all these dependencies.
\section{Polymorphic functional implementation}
\label{sect:poly_functional}
Listing~\ref{lst:polyfuncGo} gives an implementation of our test
problem, that employs Go's generics and functional features to
eliminate the last two of the rigid dependencies that were listed in
Sect.~\ref{sect:mono_functional}. The code makes use of Go's generics
to admit arrays of both the \code{int32} and \code{float64} types as
arguments to all functions, and to express the return values of the
latter. It also makes use of the run-time polymorphism inherent in
Go's advanced functional features, namely closures and variables of
higher-order functions, to replace the two previous versions of
function \code{average} (that depended on specific implementations),
by a single polymorphic version. Only the rigid dependency of function
\code{pairwise\_sum} on function \code{simple\_sum} has not been
removed, to keep the code more readable. In the OO code
versions, that will be presented in Chapter~\ref{chapt:survey}, even
this dependency is eliminated.
A few remarks are in order for a better understanding of
Listing~\ref{lst:polyfuncGo}'s code:
\begin{itemize}
\item
In Go, generic type parameters to a function, like the parameter
\code{T} here, are provided in a separate parameter list, that is
enclosed in brackets [ ].
\item
Generic type parameters have a constraint that follows their
declared name. Go exclusively uses interfaces as such constraints
(like the interface \code{INumeric} in the following code).
\item
Interfaces consist of either explicit function signatures, or
\emph{type sets}, like \code{int32 | float64} in the present
example. The latter actually signify a set of function signatures,
too, namely the signatures of the intersecting set of all the
operations and intrinsic functions for which the listed types
provide implementations.
\item
The code makes use of type conversions to the generic type \code{T},
where required. For instance, \code{T(0)} converts the (typeless)
constant \code{0} to the corresponding zero constant of type
\code{T}.
\item
The code instantiates closures and stores these by value in two
variables named \code{avi} and \code{avf} for later use (Fortran
and C programmers should note that \code{avi} and \code{avf} are
\emph{not} function pointers!).
\end{itemize}
\lstinputlisting[language=Go,style=boxed,label={lst:polyfuncGo},caption={Polymorphic functional version of the array averaging example in Go.}]{Code/Go/functional.go}
Notice how, in order to instantiate the closures \code{avi} and
\code{avf} (see the \code{switch} statement), manual instantiations of
the \code{simple\_sum} and \code{pairwise\_sum} generic functions are
required -- with the arguments \code{int32} or \code{float64} being
substituted for the generic type parameter, \code{T}, of these
functions.
The motivation to code the example as in Listing~\ref{lst:polyfuncGo}
is that once the two closures, \code{avi} and \code{avf}, are
properly instantiated, they could then be passed from the main program
to any other client code that would need to make use of the particular
averaging algorithm that was selected by the user. This latter client
code would \emph{not} have to be littered with \code{switch}
statements itself, and it would \emph{not} have to depend on any
specific implementations. It would merely depend on the closures'
interfaces. The same holds for the OO code versions that are discussed
in the next chapter, with objects replacing the closures (both being
merely slightly different realizations of the same idea).
\chapter{Survey of modern languages}
\label{chapt:survey}
In the present chapter, we give encapsulated (i.e. OO) code versions
of the test problem in various modern languages. As in the functional
code version that was presented in Sect.~\ref{sect:poly_functional},
we employ run-time polymorphism to manage the dependencies on
user-defined implementations (in this case abstract data types), and
generics in order to manage the dependencies on language-intrinsic
types. This serves to illustrate how both run-time and compile-time
polymorphism can be typically used for dependency management in an OO
setting in these modern languages. The survey also aims to highlight
the many commonalities but also some of the minor differences in the
approaches to polymorphism that were taken in these different
languages. As a final disclaimer, we do not advocate to code problems
in an OO manner that can be easily coded in these languages in a
functional way (as it is the case for this problem). However, in more
complex cases, where many more nested functions would need to be used,
and where state would have to be hidden, the OO programming style
would be the more appropriate one. Hence, our test problem will stand
in, in this chapter, for emulating such a more complex problem, that
would benefit from the OO coding style.
\section{Go}
Go has supported run-time polymorphism through (polymorphic)
``interfaces'' (and thus modern-day OO programming) since its
inception. In Go, encapsulation is done by storing state in a
\code{struct} and by binding procedures, that need to use that
state, to this same \code{struct}. Thereby creating a user-defined
abstract data type (or ADT) with methods. Go allows the programmer to
implement multiple polymorphic interfaces for such a type (i.e. to use
multiple interface inheritance), even though it offers no explicit
language statement for this purpose.
Instead, a user-defined type is implicitly assumed to implement an
interface whenever it provides implementations of all the interface's
function signatures. This structural way of implementing interfaces
also merely requires an object reference of the type to be passed to
its methods (by means of a separate parameter list, in front of a
method's actual name). It is otherwise decoupled from the type's
(i.e. the ADT's \code{struct}) definition. Go, finally, makes it
explicit in its syntax that interfaces (like \code{struct}s) are types
in their own right, and that hence polymorphic variables
(i.e. objects) can be declared in terms of them.
Restrictions in Go are that language-intrinsic types cannot have
user-provided methods, and that methods and interfaces cannot be
directly implemented for user-defined types whose definitions are
located in other packages. That is, the programmer has to write
wrappers in the latter case.
Since version 1.18, Go also supports compile-time polymorphism through
generics. Go's generics make use of ``strong concepts'', since they
are bounded by constraints that are expressed through
interfaces. Hence, the Go compiler will fully type-check generic code.
In Go, structures, interfaces, and functions, but not methods, can all
be given their own generic type parameters.
\subsection{Encapsulated version coded in Go}
\label{sect:Go_encapsulated}
Listing~\ref{lst:OOGo} gives an encapsulated version of the test
problem coded in Go. The two different implementations of the
\code{sum} function have been encapsulated in two different ADTs named
\code{SimpleSum} and \code{PairwiseSum}, whereas a third ADT named
\code{Averager} encapsulates the functionality that is required to
perform the actual averaging. The latter two ADTs contain the
lower-level objects \code{other} and \code{drv} of \code{ISum[T]} type
as components, to which they delegate calls to these objects'
\code{sum} methods. Notice, how the use of the polymorphic interface
\code{ISum[T]}, for the declarations of these objects, enables them to
be initialized with either \code{SimpleSum} or \code{PairwiseSum}
instances.
A second interface, named \code{IAverager}, is used to enable
polymorphism for different averaging algorithms. Finally, there's a
third interface, \code{INumeric}, that serves exactly the same purpose
as in the functional polymorphic version that was given in
Sect.~\ref{sect:poly_functional}, namely to make all function
arguments and return values polymorphic, by admitting as input and
output parameters both the \code{int32} and \code{float64} intrinsic
types.
Hence, three polymorphic interfaces were required in this code, to
eliminate the three levels of rigid dependencies that were listed in
Sect.~\ref{sect:mono_functional}. Notice also that, exempting
\code{INumeric}, all the interfaces and all the user-defined ADTs need
to take in generic type parameters in this example. In Go, this is
required to enable all the \code{sum} and \code{average} methods to
use such generic type parameters.
\lstinputlisting[language=Go,style=boxed,label={lst:OOGo},caption={Encapsulated Go version of the array averaging example.}]{Code/Go/mixed.go}
The main program makes use of Go's built-in structure constructors,
and chaining of their calls, to instantiate objects of the
required ADTs. In particular, it instantiates run-time polymorphic
\code{Averager} objects (depending on whether simple or pairwise sum
averaging is to take place), and it does so for both the \code{int32}
and \code{float64} types separately, to then use these
objects on \code{int32} and \code{float64} data, respectively.
That \emph{two} such objects are required (one for each
language-intrinsic data type) is connected to the aforementioned fact
that in order to make methods use generic type parameters in Go, one
has to parameterize interfaces, and instantiate these with different
actual data types, as in \code{func main}'s first two code lines. A
single (i.e. unparameterized) \code{IAverager} interface therefore
doesn't suffice, which is unfortunate from the user's perspective, as
some code duplication in client code cannot be avoided in this way.
It should also be noted, that Go's intuitive way of expressing the
generics constraints of interface \code{INumeric} in terms of a type
set, besides having obvious advantages in terms of code clarity and
conciseness, comes also at a price. Namely its only \emph{partial}
conformance to the Open/Closed Principle (OCP) of OO programming
\cite{Martin_OCP}. The latter principle demands that only new code be
added to an application for extending its functionality, and any
already written code to not be changed.
It can be seen, from Listing~\ref{lst:OOGo}, that this principle can
only be partly, but not strictly (i.e. fully), complied with in
generic object-oriented Go code. Because for the programmer to make
this listing work also with, e.g., the \code{float32} type, he would
have to add this type to the type set of interface \code{INumeric},
and to thus touch already written code. Notice, though, that
\emph{none} of the implementation code of Listing~\ref{lst:OOGo} would
be affected. Only the interface \code{INumeric} would need to be
changed, that the remaining client code depends upon as a generics
constraint.
If the client code were distributed among different packages or
modules, the consequences of such a (weak) violation of the OCP would
be confined to the occurrence of a recompilation cascade in the
dependent modules. In actual practice, one may quite often regard this
as an acceptable (minor) inconvenience, rather than a fundamental
maintenance issue -- especially in situations where the need to add
new types is a relatively rare one.
\section{Rust}
Like Go, Rust supports both run-time and compile-time polymorphism
through polymorphic interfaces, which Rust calls ``traits''. Unlike
Go, Rust has its programmers implement traits in a nominal manner, by
using explicit \code{impl} code blocks to provide a trait's method
implementations. These same \code{impl} blocks can also be used to
bind so-called ``associated functions'' to a type, which aren't
methods, i.e. which don't take in a \code{self} (passed-object dummy)
argument. A typical example for this are user-defined
constructors. See the functions that are named \code{new} in the
following code Listing~\ref{lst:OORust}, and are called using
\code{::} syntax.
In contrast to Go, Rust allows the programmer to implement traits for
both user-defined \emph{and} language-intrinsic types, and to do so
for types that are located in external libraries (called ``crates'' in
Rust), as long as the traits themselves are defined in the
programmer's own crate. The reverse, namely implementing an external
trait for a user-owned type, is also possible. Only the (edge) case of
implementing an external trait for an external type is not allowed
(this is called the ``orphan rule'' \cite{Klabnik_Nichols}). The
latter case requires the use of wrappers.
Comparable to Go, Rust's generics model allows for the generic
parameterization of functions, traits, and user-defined types like
\code{struct}s. Rust does not explicitly forbid generic
methods. However, if one defines such a method's signature within a
trait, then this will make the trait unusable for the declaration of
any ``trait objects'' \cite{Lyon}, i.e. for the employment of run-time
polymorphism. Thus, the Rust programmer will in general (need to)
parameterize traits and \code{struct}s rather than any methods
themselves. Rust generics are fully type-checked at compilation time,
i.e. Rust supports ``strong concepts''.
\subsection{Encapsulated version coded in Rust}
\label{sect:OORust}
The encapsulated Rust version of our test problem, that is given in the
following Listing~\ref{lst:OORust}, is in its outline quite similar to
the corresponding Go version\footnote{Notice that the present Rust
version makes universal use of dynamic method dispatch via trait
objects, to correspond most closely to all the other
implementations that we provide in both the present chapter, and in
Sect.~\ref{sect:Fortran_dynamic_dispatch}. An alternative, more
idiomatic, Rust version that is equivalent to the Fortran version
which we'll give in Sect.~\ref{sect:Fortran_static_dispatch}, and that
effects static dispatch of the various \code{sum} methods through the
use of generics, can be found in the \code{Code} subdirectory that is
accompanying this document.}. There are, however, a few differences,
that are listed in the following notes, and in our concluding remarks.
\begin{itemize}
\item
Rust uses angled brackets, \code{< >}, to indicate generic parameter
lists.
\item
Generics constraints in Rust are typically enforced by specifying
the required traits in \code{impl} blocks using \code{where}
statements.
\item
Rust does not offer an equivalent to Go's type set syntax and
semantics. In order to enable numeric operations on generic types,
Rust instead provides a \code{Num} trait via an external \code{num}
crate (i.e. library). However, the use of this external dependency
makes available neither the \code{AddAssign} (\code{+=}) operator,
nor casts to generic types within generic routines (as they are
provided in Go).
\item
Since the employed algorithm relies on these features, the following
code uses a homemade \code{INumeric} trait, that derives from Rust's
\code{Num} and \code{AddAssign} traits, and extends them by the
requirement to also implement a constructor via an associated
function, \code{new}, that provides the needed type casting (notice
that \code{Self} stands in here for the implementing type). The
conformance of the \code{i32} and \code{f64} intrinsic types to the
\code{INumeric} trait is then acknowledged via \code{impl} blocks,
that furthermore implement any of the still outstanding
functionality of this trait for these two types.
\item
Where necessary, use of the \code{Copy} trait is also made, to
work around Rust's default move semantics.
\item
To help make all of the source code dependencies explicit,
our Rust version employs modules, and \code{use} statements to import
the required functionality.
\item
Rust's default structure constructors exhibit the same restrictions
as Fortran's. That is, they are unable to initialize from an
external scope, structure components that are private to their
module. As in Fortran, use of user-defined constructors must be made
instead (cf. the \code{new} functions that are defined in separate
\code{impl} blocks for the ADTs \code{PairwiseSum} and
\code{Averager}).
\item
To declare run-time polymorphic variables, one has to put so-called
``trait objects'' into ``Boxes'', i.e. to declare smart pointers of
them, for dynamic instantiation and heap memory allocation (this is
the Rust equivalent to using \code{allocatable} polymorphic objects
in Fortran).
\end{itemize}
\lstinputlisting[language=Rust,style=boxed,label={lst:OORust},caption={Encapsulated Rust version of the array averaging example.}]{Code/Rust/mixed_poly/src/main.rs}
The Rust program version is somewhat longer than the corresponding Go
version, because user-defined constructors had to be provided, to
implement the aforementioned custom type conversion functionality and
to initialize module-hosted (opaque) abstract data types, and because
dependencies from external libraries, but also from modules of the user's
own crate, had to be imported into the \code{main} function (as it would
be necessary in realistic situations). The \code{main} function's
logic could also not be expressed as concisely as in the Go version,
because the various trait objects required (manual) ``boxing'' for their
instantiation, and because Rust's default move semantics had to be
worked around.
The most important difference between the two languages, though, is
that Rust uses traits (i.e. interfaces) as generics constraints whose
function signatures are always specified explicitly, whereas Go's are
typically specified implicitly (through type sets). Together with
Rust's capability (that Go lacks) to explicitly implement traits even
for language-intrinsic types, this allows Rust code to fully
(i.e. strictly) conform to OO programming's Open/Closed Principle. In
order to enable, for instance, the code of Listing~\ref{lst:OORust} to
accept also the \code{f32} type, one would simply need to add a
further module, that implements the \code{INumeric} trait for this new
intrinsic type. None of the pre-existing code would have to be
changed.
\section{Swift}
\label{sect:Swift}
Being a successor language to Objective-C, Swift differs slightly from
the languages considered so far in that it opted to retain
implementation inheritance for backwards compatibility to Objective-C,
whereas both Go and Rust do not support implementation inheritance
\emph{by design}. Swift therefore supports ``classical'' classes, but
it also allows one to bind methods to structures (which, in contrast
to classes, are value types in Swift).
Like Go and Rust, Swift (furthermore) supports a traits system
to implement both run-time and compile-time polymorphism through
polymorphic interfaces, that are called ``protocols'' in Swift. If the
Swift programmer chooses to ignore implementation inheritance and
classes, he can therefore very much program with structures and
protocols in Swift as he would with structures and interfaces/traits
in Go and Rust, respectively.
Given Swift's backwards compatible design, implementation of a
protocol (i.e. interface inheritance) is usually done as in classical
OO languages, i.e. within a structure's or a class's definition. A
colon (\code{:}) followed by one or more interface names must be
supplied for this purpose after the structure's or class's own
name. However, a very powerful facility for types to implement
protocols retroactively is also provided, through so-called
\code{extension}s, that work even if the types' source code is
inaccessible (because one is, e.g., working with a library in binary
form). In order to support strict conformance to the Open/Closed
Principle of OO programming, this same facility allows protocols to be
implemented also for language-intrinsic types. For instance, the
following little program, given by Listing~\ref{lst:extSwift}, prints
out ``\code{I am 4.9}''.
\lstinputlisting[language=Swift6,style=boxed,label={lst:extSwift},caption={Swift
example of implementing a protocol for an intrinsic data
type.}]{Code/Swift/printy.swift}
Swift generics support ``strong concepts'', and are thus fully
type-checked at compile time, and their capabilities are at least on
par with those of Go and Rust. In several aspects they are
actually superior. For instance, Swift allows function overloading to
be used together with generics \cite{Swift_Generics,Lyon}. Swift also
allows for parameterized \emph{methods}, instead of parameterized
protocols. The latter point has some interesting, positive
implications for the Swift programmer, that will be discussed in
detail below.
\subsection{Encapsulated version coded in Swift}
Listing~\ref{lst:OOSwift} gives an example of how the encapsulated
version of the array averaging test problem can be programmed in
Swift. See the following remarks, to understand this code.
\begin{itemize}
\item
By default, function and method calls in Swift need to make use
of mandatory keyword arguments.
\item
Array slices are not arrays themselves. Hence, an explicit
conversion of such slices via an \code{Array()} constructor is
required to use them as arrays.
\item
Like Rust, Swift uses angled brackets to indicate generic
parameter lists. Type constraints are formulated within these
lists by supplying a protocol name after a generic type parameter
(separated by a colon).
\item
Similar to Rust, Swift provides a \code{Numeric} protocol (that is
defined in its standard library) for use as a generics constraint
for numeric types, which however does \emph{not} include the
division operation!
\item
It is easy to extend this protocol to support also the division
operator, similar to the way we've extended traits in the Rust
code example. However, the following code goes a step further than
that, and foregoes any dependence on library functionality
whatsoever, by defining a custom \code{INumeric} protocol that
prescribes the interfaces of all the required operators and type
initializers (i.e. constructors) explicitly.
\item
The present Swift code makes use of the language's default,
built-in, initializers for the intrinsic types and the
\code{SimpleSum} \code{struct} that it employs, and user-defined
initializers for all the \code{struct}s that need to initialize
\code{private} field variables.
\item
Type conversion into a generic type, \code{T}, is effected in
Swift by calling an appropriate initiali\-zer of this type. The
call syntax is equivalent to that of Go's generic casts, but is a
bit peculiar in that, e.g., Go's \code{T(0)} is written as
\code{T(exactly:0)!} in Swift (making use of both the [default]
initializer's mandatory keyword, \code{exactly}, and the \code{!}
operator to unwrap the returned optional type). Notice, how the
initializer's signature needs to be prescribed in protocol
\code{INumeric}, in order to be able to use this functionality
with some conforming type \code{T}.
\item
Swift's \code{Int32} and \code{Float64} intrinsic types already
implement all of protocol \code{INumeric}'s functionality by
default. It therefore suffices to simply acknowledge this fact
through empty \code{extension} statements for these two types.
\end{itemize}
\newpage
\lstinputlisting[language=Swift6,style=boxed,label={lst:OOSwift},caption={Encapsulated Swift version of the array averaging example.}]{Code/Swift/mixed.swift}
Even a casual glance at the Swift version will show that the Swift
code is the easiest to read and understand among the generic
object-oriented implementations that were presented in this
chapter. This is largely the result of Swift supporting generic
methods, and hence not requiring the programmer to parameterize and
instantiate any generic interfaces (protocols), in contrast to both Go
and Rust. The consequences are
\begin{itemize}
\item
that method genericity for an ADT's objects can be expressed using
only a single, as opposed to multiple protocols,
\item
that therefore merely a \emph{single} object instance of that same
protocol is required, to be able to operate on many different
language-intrinsic data types, and
\item
that this also (largely) \emph{obviates the need for manual
instantiations of generics in Swift} (because generic
functions/methods are easier to instantiate automatically by the
compiler, as it can almost always infer the required types by checking
the regular arguments that are passed to a function/method)!
\end{itemize}
As an example, consider the object \code{av} in the above Swift code
that contains the functionality for array averaging. This object
supports two different levels of polymorphism: Firstly, given that it
is an instance of the \code{IAverager} protocol, it can be
polymorphically assigned different averaging algorithms (see the
\code{switch} statement). Secondly, because it contains an
\code{average} method that is generic, it can be used on data of
different intrinsic types, like \code{Int32} and \code{Float64} here.
Notice that the \code{main} function in the Swift code needs to
declare merely a \emph{single} such object variable of
\code{IAverager} type, to make use of all these capabilities. This is
a direct consequence of there being only a single
(i.e. unparameterized) version of the \code{IAverager} protocol, and
of parameterizing the protocol's method signatures by generic types
rather than the protocol itself.
Contrast this with Go's and Rust's model, where not only separate
objects of \code{IAverager} type are required for \emph{every}
different intrinsic data type that the programmer wishes to use these
objects with. But where also \emph{manual} instantiations of
corresponding versions of the generically parameterized
\code{IAverager} interface/trait are required from the programmer, for
declaring these objects. Swift's generics model gets rid of all of
that complexity, and therefore vastly simplifies client code. We
consider this a very significant advantage of the generics approach
that is taken in Swift vs. that of Go and Rust.
\section{Conclusions}
The use of run-time polymorphism by means of (polymorphic) interfaces
is rather similar in all the languages that were considered here. The
most significant difference in this respect is that Go has stricter
limitations than the other languages regarding code ownership, when it
comes to retroactively implementing interfaces for existing
types. Whereas Rust (with some minor restrictions), and Swift allow
the implementation of an interface by some type to be accomplished
effectively without regard to the type's definition site. Rust and
Swift thereby overcome Haveraaen et al.'s critique
\cite{Haveraaen_et_al_19} of Java regarding this point. In fact, it is
\emph{interface inheritance} which makes the uniform polymorphic
treatment of both intrinsic and user-defined types possible in the
first place in Rust and Swift, that Haveraaen et al. seem to also
(rightly) demand. All the considered languages are also quite similar
in that they support fully type checked generics via the mechanism of
interfaces. In the following, we will thus focus on summarizing the
most significant differences in these languages' generics features.
\subsection{Go}
Go's basic model to implement generics allows structures, interfaces,
and ordinary functions, but not methods, to be given their own generic
type parameters. The lack of true generic methods makes some
duplication of instantiation code in clients
unavoidable. Nevertheless, generic Go code is quite easy to read and
to understand. Go features built-in, easy to use support for
conversion to generic types. Yet, where Go truly differs from both
Rust and Swift are the unique, and extremely useful, implicit
mechanisms for conformance to interfaces that it supports, namely
structural subtyping, and the brilliant new notion of formulating
interfaces in terms of type sets, to which the member types of these
sets conform by definition.
The latter idea, along with the corresponding syntax to support it,
enables the Go programmer to formulate generics constraints very
naturally and concisely, without having to explicitly implement
interfaces for this purpose. This is what makes the use of generics in
Go pleasant. It is, moreover, essential for supporting concise generic
procedural and functional programming. While type sets retain their
advantages also in generic object-oriented programming, their
disadvantage in this setting is that they allow for only partial, but
not full, conformance to object-orientation's Open/Closed Principle.
\subsection{Rust}
Rust's basic model for generics is similar to Go's in that it allows
for parameterization of structures, interfaces, and ordinary
functions, but not necessarily methods. Hence, what has been said
above for Go in this respect holds also for Rust. Rust has,
unfortunately, some quirks which render its use for the management of
all types of dependencies through polymorphism somewhat sub-optimal
when compared to the other languages considered here. The language is
unpleasant to use, because of its ``borrow checker'', its employment
of move semantics by default, its \emph{excessive} obsession with type
safety, and its overall C++-like philosophy to copiously rely on
external dependencies, even for the most basic tasks.
Because of the latter two points, the Rust version of our test case is
marred by dependencies on external libraries, which is quite
contrarian to the purpose of programming in a polymorphic fashion,
namely to avoid rigid dependencies. Even with the functionality
provided by these external dependencies, casts to generic types within
generic routines weren't outright possible, and had to be achieved,
instead, through some work on the programmer's side. The points we
like most about the language are its idea to decouple trait
(i.e. interface) implementations from a \code{struct}'s definition
through \code{impl} blocks that work with both user-defined and
intrinsic types, the full conformance to the Open/Closed Principle
that this enables, and the complete control over the use of dynamic
vs. static method dispatch (via trait objects and generics,
respectively) that Rust affords the programmer. These are the features
of Rust that, in our opinion, Fortran should borrow in some form.
\subsection{Swift}
Swift's basic model of implementing generics by allowing parameterized
structures, functions, and methods (but not parameterized interfaces)
is both the easiest to read, and the easiest to use from a
programmer's perspective. In this model, if a programmer knows how to
write generic functions, his knowledge automatically translates into
coding generic methods. Swift's generics design supports casts to
generic types, regardless of whether these types are user- or
language-defined. Moreover, the Swift compiler is able to instantiate
generics largely automatically, through type inference of the regular
arguments that are passed to functions, methods, and (structure or
class) constructors. In contrast to the other languages, in Swift, the
user almost never has to bother with instantiating any generics.
Like Rust, Swift enables explicit and retroactive implementation of
interfaces for both user-defined and intrinsic types, and the full
conformance to the Open/Closed Principle of OOP that this affords
one. Unlike Rust and Go, Swift offers generics that are interoperable
with both class inheritance and ad hoc polymorphism (i.e. function
overloading), which are two features that are also an integral part of
Fortran. Out of all the considered modern languages, Swift therefore
shares the largest similarities with Fortran. Moreover, there is
notable documentation available
\cite{Swift_Generics,Swift_Generics_Book} on the model with which
Swift's generics are implemented in the Swift compiler. Finally, and
very importantly, with this implementation model, compilation times
scale only with the amount of generic code \emph{written}, rather than
the amount of code \emph{instantiated} \cite{Swift_Generics}. This is
in stark contrast to templated approaches (as used e.g. in C++).
For all these reasons, we consider Swift's generics to be the by far
most attractive model to base Fortran's generics capabilities
on. The fact that Swift is a language that does not put emphasis on
numerics, and whose present standard library therefore does not
provide a truly useful \code{Numeric} protocol/interface, is of
absolutely \emph{no} consequence for adopting Swift's generics design
as a baseline for Fortran. We believe, though, that this baseline
design would benefit substantially from being supplemented with Go's
implicit mechanisms for interface conformance.
Especially for properly supporting generic procedural and functional
programming in Fortran, i.e. for making them \emph{sufficiently
concise}, Go-like type sets should be added. These would also be
useful in generic object-oriented programming, even though Swift's