1 | \chapter{Introduction} |
---|
2 | |
---|
3 | This thesis is exploratory work I did to understand, fix, and extend the \CFA type-system, specifically, the type resolver used to select polymorphic types among overloaded names. |
---|
4 | Overloading allows programmers to use the most meaningful names without fear of name clashes within a program or from external sources, like include files. |
---|
5 | \begin{quote} |
---|
6 | There are only two hard things in Computer Science: cache invalidation and \emph{naming things}. --- Phil Karlton |
---|
7 | \end{quote} |
---|
8 | Experience from \CC and \CFA developers is that the type system implicitly and correctly disambiguates the majority of overloaded names, \ie it is rare to get an incorrect selection or ambiguity, even among hundreds of overloaded (variables and) functions. |
---|
9 | In many cases, a programmer has no idea there are name clashes, as they are silently resolved, simplifying the development process. |
---|
10 | Depending on the language, any ambiguous cases are resolved using some form of qualification and/or casting. |
---|
11 | |
---|
12 | Therefore, one of the key goals in \CFA is to push the boundary on overloading, and hence, overload resolution. |
---|
13 | As well, \CFA follows the current trend of replacing nominal inheritance with traits. |
---|
14 | Together, the resulting \CFA type-system has a number of unique features making it different from all other programming languages. |
---|
15 | |
---|
16 | |
---|
17 | \section{Types} |
---|
18 | |
---|
19 | \begin{quote} |
---|
20 | Some are born great, some achieve greatness, and some have greatness thrust upon them. Twelfth Night, Act II Scene 5, William Shakespeare |
---|
21 | \end{quote} |
---|
22 | |
---|
23 | All computers have multiple types because computer architects optimize the hardware around a few basic types with well defined (mathematical) operations: boolean, integral, floating-point, and occasionally strings. |
---|
24 | A programming language and its compiler present ways to declare types that ultimately map into the ones provided by the underlying hardware. |
---|
25 | These language types are thrust upon programmers, with their syntactic and semantic rules and restrictions. |
---|
26 | These rules are used to transform a language expression to a hardware expression. |
---|
27 | Modern programming-languages allow user-defined types and generalize across multiple types using polymorphism. |
---|
28 | Type systems can be static, where each variable has a fixed type during execution and an expression's type is determined once at compile time, or dynamic, where each variable can change type during execution and so an expression's type is reconstructed on each evaluation. |
---|
29 | Expressibility, generalization, and safety are all bound up in a language's type system, and hence, directly affect the capability, build time, and correctness of program development. |
---|
30 | |
---|
31 | |
---|
32 | \section{Operator Overloading} |
---|
33 | |
---|
34 | Virtually all programming languages overload the arithmetic operators across the basic computational types using the number and type of parameters and returns. |
---|
35 | Like \CC, \CFA also allows these operators to be overloaded with user-defined types. |
---|
36 | The syntax for operator names uses the @'?'@ character to denote a parameter, \eg unary left and right operators: @?++@ and @++?@, and binary operators @?+?@ and @?<=?@. |
---|
37 | Here, a user-defined type is extended with an addition operation with the same syntax as builtin types. |
---|
38 | \begin{cfa} |
---|
39 | struct S { int i, j }; |
---|
40 | S @?+?@( S op1, S op2 ) { return (S){ op1.i + op2.i, op1.j + op2.j }; } |
---|
41 | S s1, s2; |
---|
42 | s1 = s1 @+@ s2; $\C[1.75in]{// infix call}$ |
---|
43 | s1 = @?+?@( s1, s2 ); $\C{// direct call}\CRT$ |
---|
44 | \end{cfa} |
---|
45 | The type system examines each call site and selects the best matching overloaded function based on the number and types of arguments. |
---|
46 | If there are mixed-mode operands, @2 + 3.5@, the type system, like in C/\CC, attempts (safe) conversions, converting the argument type(s) to the parameter type(s). |
---|
47 | Conversions are necessary because the hardware rarely supports mix-mode operations, so both operands must be the same type. |
---|
48 | Note, without implicit conversions, programmers must write an exponential number of functions covering all possible exact-match cases among all possible types. |
---|
49 | This approach does not match with programmer intuition and expectation, regardless of any \emph{safety} issues resulting from converted values. |
---|
50 | |
---|
51 | |
---|
52 | \section{Function Overloading} |
---|
53 | |
---|
54 | Both \CFA and \CC allow function names to be overloaded, as long as their prototypes differ in the number and type of parameters and returns. |
---|
55 | \begin{cfa} |
---|
56 | void f( void ); $\C[2in]{// (1): no parameter}$ |
---|
57 | void f( char ); $\C{// (2): overloaded on the number and parameter type}$ |
---|
58 | void f( int, int ); $\C{// (3): overloaded on the number and parameter type}$ |
---|
59 | f( 'A' ); $\C{// select (2)}\CRT$ |
---|
60 | \end{cfa} |
---|
61 | In this case, the name @f@ is overloaded depending on the number and parameter types. |
---|
62 | The type system examines each call size and selects the best match based on the number and types of the arguments. |
---|
63 | Here, there is a perfect match for the call, @f( 'A' )@ with the number and parameter type of function (2). |
---|
64 | |
---|
65 | Ada, Scala, and \CFA type-systems also use the return type in resolving a call, to pinpoint the best overloaded name. |
---|
66 | For example, in many programming languages with overloading, the following functions are ambiguous without using the return type. |
---|
67 | \begin{cfa} |
---|
68 | int f( int ); $\C[2in]{// (1); overloaded on return type and parameter}$ |
---|
69 | double f( int ); $\C{// (2); overloaded on return type and parameter}$ |
---|
70 | int i = f( 3 ); $\C{// select (1)}$ |
---|
71 | double d = f( 3 ); $\C{// select (2)}\CRT$ |
---|
72 | \end{cfa} |
---|
73 | Alternatively, if the type system looks at the return type, there is an exact match for each call, which matches with programmer intuition and expectation. |
---|
74 | This capability can be taken to the extreme, where there are no function parameters. |
---|
75 | \begin{cfa} |
---|
76 | int random( void ); $\C[2in]{// (1); overloaded on return type}$ |
---|
77 | double random( void ); $\C{// (2); overloaded on return type}$ |
---|
78 | int i = random(); $\C{// select (1)}$ |
---|
79 | double d = random(); $\C{// select (2)}\CRT$ |
---|
80 | \end{cfa} |
---|
81 | Again, there is an exact match for each call. |
---|
82 | If there is no exact match, a set of minimal conversions can be added to find a best match, as for operator overloading. |
---|
83 | |
---|
84 | |
---|
85 | \section{Variable Overloading} |
---|
86 | |
---|
87 | Unlike most programming languages, \CFA has variable overloading within a scope, along with shadow overloading in nested scopes. |
---|
88 | (Shadow overloading is also possible for functions, if a language supports nested function declarations, \eg \CC named, nested, lambda functions.) |
---|
89 | \begin{cfa} |
---|
90 | void foo( double d ); |
---|
91 | int v; $\C[2in]{// (1)}$ |
---|
92 | double v; $\C{// (2) variable overloading}$ |
---|
93 | foo( v ); $\C{// select (2)}$ |
---|
94 | { |
---|
95 | int v; $\C{// (3) shadow overloading}$ |
---|
96 | double v; $\C{// (4) and variable overloading}$ |
---|
97 | foo( v ); $\C{// select (4)}\CRT$ |
---|
98 | } |
---|
99 | \end{cfa} |
---|
100 | It is interesting that shadow overloading is considered a normal programming-language feature with only slight software-engineering problems. |
---|
101 | However, variable overloading within a scope is considered extremely dangerous, without any evidence to corroborate this claim. |
---|
102 | Similarly, function overloading occurs silently within the global scope in \CC from @#include@ files all the time without problems. |
---|
103 | |
---|
104 | In \CFA, the type system simply treats overloaded variables as an overloaded function returning a value with no parameters. |
---|
105 | Hence, no significant effort is required to support this feature by leveraging the return type to disambiguate as variables have no parameters. |
---|
106 | \begin{cfa} |
---|
107 | int MAX = 2147483647; $\C[2in]{// (1); overloaded on return type}$ |
---|
108 | long int MAX = ...; $\C{// (2); overloaded on return type}$ |
---|
109 | double MAX = ...; $\C{// (3); overloaded on return type}$ |
---|
110 | int i = MAX; $\C{// select (1)}$ |
---|
111 | long int i = MAX; $\C{// select (2)}$ |
---|
112 | double d = MAX; $\C{// select (3)}\CRT$ |
---|
113 | \end{cfa} |
---|
114 | Hence, the name @MAX@ can replace all the C type-specific names, \eg @INT_MAX@, @LONG_MAX@, @DBL_MAX@, \etc. |
---|
115 | The result is a significant reduction in names to access typed constants. |
---|
116 | % Paraphrasing Shakespeare, ``The \emph{name} is the thing.''. |
---|
117 | |
---|
118 | |
---|
119 | \section{Constant Overloading} |
---|
120 | |
---|
121 | \CFA is unique in providing restricted constant overloading for the values @0@ and @1@, which have special status in C, \eg the value @0@ is both an integer and a pointer literal, so its meaning depends on context. |
---|
122 | In addition, several operations are defined in terms of values @0@ and @1@, \eg: |
---|
123 | \begin{cfa} |
---|
124 | if ( x ) ++x $\C{// if ( x != 0 ) x += 1;}$ |
---|
125 | \end{cfa} |
---|
126 | Every @if@ and iteration statement in C compares the condition with @0@, and every increment and decrement operator is semantically equivalent to adding or subtracting the value @1@ and storing the result. |
---|
127 | These two constants are given types @zero_t@ and @one_t@ in \CFA, which allows overloading various operations for new types that seamlessly connect to all special @0@ and @1@ contexts. |
---|
128 | The types @zero_t@ and @one_t@ have special builtin implicit conversions to the various integral types, and a conversion to pointer types for @0@, which allows standard C code involving @0@ and @1@ to work. |
---|
129 | \begin{cfa} |
---|
130 | struct S { int i, j; }; |
---|
131 | void ?{}( S & s, zero_t ) { s.[i,j] = 0; } $\C{// constructors}$ |
---|
132 | void ?{}( S & s, one_t ) { s.[i,j] = 1; } |
---|
133 | S ?=?( S & dst, zero_t ) { dst.[i,j] = 0; return dst; } $\C{// assignment}$ |
---|
134 | S ?=?( S & dst, one_t ) { dst.[i,j] = 1; return dst; } |
---|
135 | S ?+=?( S & s, one_t ) { s.[i,j] += 1; return s; } $\C{// increment/decrement}$ |
---|
136 | S ?-=?( S & s, one_t ) { s.[i,j] -= 1; return s; } |
---|
137 | int ?!=?( S s, zero_t ) { return s.i != 0 && s.j != 0; } $\C{// comparison}$ |
---|
138 | S s = @0@; |
---|
139 | s = @0@; |
---|
140 | s = @1@; |
---|
141 | if ( @s@ ) @++s@; $\C{// unary ++/-\,- come from +=/-=}$ |
---|
142 | \end{cfa} |
---|
143 | Hence, type @S@ is first-class with respect to the basic types, working with all existing implicit C mechanisms. |
---|
144 | |
---|
145 | |
---|
146 | \section{Type Inferencing} |
---|
147 | |
---|
148 | Every variable has a type, but association between them can occur in different ways: |
---|
149 | at the point where the variable comes into existence (declaration) and/or on each assignment to the variable. |
---|
150 | \begin{cfa} |
---|
151 | double x; $\C{// type only}$ |
---|
152 | float y = 3.1D; $\C{// type and initialization}$ |
---|
153 | auto z = y; $\C{// initialization only}$ |
---|
154 | z = "abc"; $\C{// assignment}$ |
---|
155 | \end{cfa} |
---|
156 | For type-only, the programmer specifies the initial type, which remains fixed for the variable's lifetime in statically typed languages. |
---|
157 | For type-and-initialization, the specified and initialization types may not agree. |
---|
158 | For initialization-only, the compiler may select the type by melding programmer and context information. |
---|
159 | When the compiler participates in type selection, it is called \newterm{type inferencing}. |
---|
160 | Note, type inferencing is different from type conversion: type inferencing \emph{discovers} a variable's type before setting its value, whereas conversion has two typed values and performs a (possibly lossy) action to convert one value to the type of the other variable. |
---|
161 | Finally, for assignment, the current variable and expression types may not agree. |
---|
162 | |
---|
163 | One of the first and powerful type-inferencing system is Hindley--Milner~\cite{Damas82}. |
---|
164 | Here, the type resolver starts with the types of the program constants used for initialization and these constant types flow throughout the program, setting all variable and expression types. |
---|
165 | \begin{cfa} |
---|
166 | auto f() { |
---|
167 | x = 1; y = 3.5; $\C{// set types from constants}$ |
---|
168 | x = // expression involving x, y and other local initialized variables |
---|
169 | y = // expression involving x, y and other local initialized variables |
---|
170 | return x, y; |
---|
171 | } |
---|
172 | auto w = f(); $\C{// typing flows outwards}$ |
---|
173 | |
---|
174 | void f( auto x, auto y ) { |
---|
175 | x = // expression involving x, y and other local initialized variables |
---|
176 | y = // expression involving x, y and other local initialized variables |
---|
177 | } |
---|
178 | s = 1; t = 3.5; $\C{// set types from constants}$ |
---|
179 | f( s, t ); $\C{// typing flows inwards}$ |
---|
180 | \end{cfa} |
---|
181 | In both overloads of @f@, the type system works from the constant initializations inwards and/or outwards to determine the types of all variables and functions. |
---|
182 | Note, like template meta-programming, there could be a new function generated for the second @f@ depending on the types of the arguments, assuming these types are meaningful in the body of @f@. |
---|
183 | Inferring type constraints, by analysing the body of @f@ is possible, and these constraints must be satisfied at each call site by the argument types; |
---|
184 | in this case, parametric polymorphism can allow separate compilation. |
---|
185 | In languages with type inferencing, there is often limited overloading to reduce the search space, which introduces the naming problem. |
---|
186 | Note, return-type inferencing goes in the opposite direction to Hindley--Milner: knowing the type of the result and flowing back through an expression to help select the best possible overloads, and possibly converting the constants for a best match. |
---|
187 | |
---|
188 | In simpler type-inferencing systems, such as C/\CC/\CFA, there are more specific usages. |
---|
189 | \begin{cquote} |
---|
190 | \setlength{\tabcolsep}{10pt} |
---|
191 | \begin{tabular}{@{}lll@{}} |
---|
192 | \multicolumn{1}{c}{\textbf{gcc / \CFA}} & \multicolumn{1}{c}{\textbf{\CC}} \\ |
---|
193 | \begin{cfa} |
---|
194 | #define expr 3.0 * i |
---|
195 | typeof(expr) x = expr; |
---|
196 | int y; |
---|
197 | typeof(y) z = y; |
---|
198 | \end{cfa} |
---|
199 | & |
---|
200 | \begin{cfa} |
---|
201 | |
---|
202 | auto x = 3.0 * 4; |
---|
203 | int y; |
---|
204 | auto z = y; |
---|
205 | \end{cfa} |
---|
206 | & |
---|
207 | \begin{cfa} |
---|
208 | |
---|
209 | // use type of initialization expression |
---|
210 | |
---|
211 | // use type of initialization expression |
---|
212 | \end{cfa} |
---|
213 | \end{tabular} |
---|
214 | \end{cquote} |
---|
215 | The two important capabilities are: |
---|
216 | \begin{itemize}[topsep=0pt] |
---|
217 | \item |
---|
218 | Not determining or writing long generic types, \eg, given deeply nested generic types. |
---|
219 | \begin{cfa} |
---|
220 | typedef T1(int).T2(float).T3(char).T @ST@; $\C{// \CFA nested type declaration}$ |
---|
221 | @ST@ x, y, x; |
---|
222 | \end{cfa} |
---|
223 | This issue is exaggerated with \CC templates, where type names are 100s of characters long, resulting in unreadable error messages. |
---|
224 | \item |
---|
225 | Ensuring the type of secondary variables, always match a primary variable. |
---|
226 | \begin{cfa} |
---|
227 | int x; $\C{// primary variable}$ |
---|
228 | typeof(x) y, z, w; $\C{// secondary variables match x's type}$ |
---|
229 | \end{cfa} |
---|
230 | If the type of @x@ changes, the type of the secondary variables correspondingly updates. |
---|
231 | \end{itemize} |
---|
232 | Note, the use of @typeof@ is more restrictive, and possibly safer, than general type-inferencing. |
---|
233 | \begin{cfa} |
---|
234 | int x; |
---|
235 | type(x) y = ... // complex expression |
---|
236 | type(x) z = ... // complex expression |
---|
237 | \end{cfa} |
---|
238 | Here, the types of @y@ and @z@ are fixed (branded), whereas with type inferencing, the types of @y@ and @z@ are potentially unknown. |
---|
239 | |
---|
240 | |
---|
241 | \section{Type-Inferencing Issues} |
---|
242 | |
---|
243 | Each kind of type-inferencing system has its own set of issues that flow onto the programmer in the form of convenience, restrictions, or confusions. |
---|
244 | |
---|
245 | A convenience is having the compiler use its overarching program knowledge to select the best type for each variable based on some notion of \emph{best}, which simplifies the programming experience. |
---|
246 | |
---|
247 | A restriction is the conundrum in type inferencing of when to \emph{brand} a type. |
---|
248 | That is, when is the type of the variable/function more important than the type of its initialization expression. |
---|
249 | For example, if a change is made in an initialization expression, it can cause cascading type changes and/or errors. |
---|
250 | At some point, a variable's type needs to remain constant and the initializing expression needs to be modified or in error when it changes. |
---|
251 | Often type-inferencing systems allow restricting (\newterm{branding}) a variable or function type, so the complier can report a mismatch with the constant initialization. |
---|
252 | \begin{cfa} |
---|
253 | void f( @int@ x, @int@ y ) { // brand function prototype |
---|
254 | x = // expression involving x, y and other local initialized variables |
---|
255 | y = // expression involving x, y and other local initialized variables |
---|
256 | } |
---|
257 | s = 1; t = 3.5; |
---|
258 | f( s, @t@ ); // type mismatch |
---|
259 | \end{cfa} |
---|
260 | In Haskell, it is common for programmers to brand (type) function parameters. |
---|
261 | |
---|
262 | A confusion is large blocks of code where all declarations are @auto@, as is now common in \CC. |
---|
263 | As a result, understanding and changing the code becomes almost impossible. |
---|
264 | Types provide important clues as to the behaviour of the code, and correspondingly to correctly change or add new code. |
---|
265 | In these cases, a programmer is forced to re-engineer types, which is fragile, or rely on a fancy IDE that can re-engineer types for them. |
---|
266 | For example, given: |
---|
267 | \begin{cfa} |
---|
268 | auto x = @...@ |
---|
269 | \end{cfa} |
---|
270 | and the need to write a routine to compute using @x@ |
---|
271 | \begin{cfa} |
---|
272 | void rtn( @type of x@ parm ); |
---|
273 | rtn( x ); |
---|
274 | \end{cfa} |
---|
275 | A programmer must re-engineer the type of @x@'s initialization expression, reconstructing the possibly long generic type-name. |
---|
276 | In this situation, having the type name or its short alias is essential. |
---|
277 | |
---|
278 | The \CFA's type system tries to prevent type-resolution mistakes by relying heavily on the type of the left-hand side of assignment to pinpoint the right types within an expression. |
---|
279 | Type inferencing defeats this goal because there is no left-hand type. |
---|
280 | Fundamentally, type inferencing tries to magic away variable types from the programmer. |
---|
281 | However, this results in lazy programming with the potential for poor performance and safety concerns. |
---|
282 | Types are as important as control-flow in writing a good program, and should not be masked, even if it requires the programmer to think! |
---|
283 | A similar issue is garbage collection, where storage management is magicked away, often resulting in poor program design and performance.\footnote{ |
---|
284 | There are full-time Java consultants, who are hired to find memory-management problems in large Java programs.} |
---|
285 | The entire area of Computer-Science data-structures is obsessed with time and space, and that obsession should continue into regular programming. |
---|
286 | Understanding space and time issues are an essential part of the programming craft. |
---|
287 | Given @typedef@ and @typeof@ in \CFA, and the strong need to use the left-hand type in resolution, implicit type-inferencing is unsupported. |
---|
288 | Should a significant need arise, this feature can be revisited. |
---|
289 | |
---|
290 | |
---|
291 | \section{Polymorphism} |
---|
292 | |
---|
293 | \CFA provides polymorphic functions and types, where the polymorphic function can be the constraints types using assertions based on traits. |
---|
294 | |
---|
295 | \subsection{\texorpdfstring{\protect\lstinline{forall} functions}{forall functions}} |
---|
296 | \label{sec:poly-fns} |
---|
297 | |
---|
298 | The signature feature of \CFA is parametric-polymorphic functions~\cite{forceone:impl,Cormack90,Duggan96} with functions generalized using a @forall@ clause (giving the language its name). |
---|
299 | \begin{cfa} |
---|
300 | @forall( T )@ T identity( T val ) { return val; } |
---|
301 | int forty_two = identity( 42 ); $\C{// T is bound to int, forty\_two == 42}$ |
---|
302 | \end{cfa} |
---|
303 | This @identity@ function can be applied to any complete \newterm{object type} (or @otype@). |
---|
304 | The type variable @T@ is transformed into a set of additional implicit parameters encoding sufficient information about @T@ to create and return a variable of that type. |
---|
305 | The \CFA implementation passes the size and alignment of the type represented by an @otype@ parameter, as well as an assignment operator, constructor, copy constructor, and destructor. |
---|
306 | If this extra information is not needed, for instance, for a pointer, the type parameter can be declared as a \newterm{data type} (or @dtype@). |
---|
307 | |
---|
308 | In \CFA, the polymorphic runtime cost is spread over each polymorphic call, because more arguments are passed to polymorphic functions; |
---|
309 | the experiments in Section~\ref{sec:eval} show this overhead is similar to \CC virtual function calls. |
---|
310 | A design advantage is that, unlike \CC template functions, \CFA polymorphic functions are compatible with C \emph{separate compilation}, preventing compilation and code bloat. |
---|
311 | |
---|
312 | Since bare polymorphic types provide a restricted set of available operations, \CFA provides a \newterm{type assertion}~\cite[pp.~37-44]{Alphard} mechanism to provide further type information, where type assertions may be variable or function declarations that depend on a polymorphic type variable. |
---|
313 | For example, the function @twice@ can be defined using the \CFA syntax for operator overloading. |
---|
314 | \begin{cfa} |
---|
315 | forall( T @| { T ?+?(T, T); }@ ) T twice( T x ) { return x @+@ x; } $\C{// ? denotes operands}$ |
---|
316 | int val = twice( twice( 3.7 ) ); $\C{// val == 14}$ |
---|
317 | \end{cfa} |
---|
318 | This works for any type @T@ with a matching addition operator. |
---|
319 | The polymorphism is achieved by creating a wrapper function for calling @+@ with the @T@ bound to @double@ and then passing this function to the first call of @twice@. |
---|
320 | There is now the option of using the same @twice@ and converting the result into @int@ on assignment or creating another @twice@ with the type parameter @T@ bound to @int@ because \CFA uses the return type~\cite{Cormack81,Baker82,Ada} in its type analysis. |
---|
321 | The first approach has a late conversion from @double@ to @int@ on the final assignment, whereas the second has an early conversion to @int@. |
---|
322 | \CFA minimizes the number of conversions and their potential to lose information; |
---|
323 | hence, it selects the first approach, which corresponds with C programmer intuition. |
---|
324 | |
---|
325 | Crucial to the design of a new programming language are the libraries to access thousands of external software features. |
---|
326 | Like \CC, \CFA inherits a massive compatible library base, where other programming languages must rewrite or provide fragile interlanguage communication with C. |
---|
327 | A simple example is leveraging the existing type-unsafe (@void *@) C @bsearch@ to binary search a sorted float array. |
---|
328 | \begin{cfa} |
---|
329 | void * bsearch( const void * key, const void * base, size_t nmemb, size_t size, |
---|
330 | int (* compar)( const void *, const void * )); |
---|
331 | int comp( const void * t1, const void * t2 ) { |
---|
332 | return *(double *)t1 < *(double *)t2 ? -1 : *(double *)t2 < *(double *)t1 ? 1 : 0; |
---|
333 | } |
---|
334 | double key = 5.0, vals[10] = { /* 10 sorted float values */ }; |
---|
335 | double * val = (double *)bsearch( &key, vals, 10, sizeof(vals[0]), comp ); $\C{// search sorted array}$ |
---|
336 | \end{cfa} |
---|
337 | This can be augmented simply with generalized, type-safe, \CFA-overloaded wrappers. |
---|
338 | \begin{cfa} |
---|
339 | forall( T | { int ?<?( T, T ); } ) T * bsearch( T key, const T * arr, size_t size ) { |
---|
340 | int comp( const void * t1, const void * t2 ) { /* as above with double changed to T */ } |
---|
341 | return (T *)bsearch( &key, arr, size, sizeof(T), comp ); |
---|
342 | } |
---|
343 | forall( T | { int ?<?( T, T ); } ) unsigned int bsearch( T key, const T * arr, size_t size ) { |
---|
344 | T * result = bsearch( key, arr, size ); $\C{// call first version}$ |
---|
345 | return result ? result - arr : size; $\C{// pointer subtraction includes sizeof(T)}$ |
---|
346 | } |
---|
347 | double * val = bsearch( 5.0, vals, 10 ); $\C{// selection based on return type}$ |
---|
348 | int posn = bsearch( 5.0, vals, 10 ); |
---|
349 | \end{cfa} |
---|
350 | The nested function @comp@ provides the hidden interface from typed \CFA to untyped (@void *@) C, plus the cast of the result. |
---|
351 | % FIX |
---|
352 | Providing a hidden @comp@ function in \CC is awkward as lambdas do not use C calling conventions and template declarations cannot appear in block scope. |
---|
353 | In addition, an alternate kind of return is made available: position versus pointer to found element. |
---|
354 | \CC's type system cannot disambiguate between the two versions of @bsearch@ because it does not use the return type in overload resolution, nor can \CC separately compile a template @bsearch@. |
---|
355 | |
---|
356 | \CFA has replacement libraries condensing hundreds of existing C functions into tens of \CFA overloaded functions, all without rewriting the actual computations (see Section~\ref{sec:libraries}). |
---|
357 | For example, it is possible to write a type-safe \CFA wrapper @malloc@ based on the C @malloc@, where the return type supplies the type/size of the allocation, which is impossible in most type systems. |
---|
358 | \begin{cfa} |
---|
359 | forall( T & | sized(T) ) T * malloc( void ) { return (T *)malloc( sizeof(T) ); } |
---|
360 | // select type and size from left-hand side |
---|
361 | int * ip = malloc(); double * dp = malloc(); struct S {...} * sp = malloc(); |
---|
362 | \end{cfa} |
---|
363 | |
---|
364 | Call site inferencing and nested functions provide a localized form of inheritance. |
---|
365 | For example, the \CFA @qsort@ only sorts in ascending order using @<@. |
---|
366 | However, it is trivial to locally change this behavior. |
---|
367 | \begin{cfa} |
---|
368 | forall( T | { int ?<?( T, T ); } ) void qsort( const T * arr, size_t size ) { /* use C qsort */ } |
---|
369 | int main() { |
---|
370 | int ?<?( double x, double y ) { return x @>@ y; } $\C{// locally override behavior}$ |
---|
371 | qsort( vals, 10 ); $\C{// descending sort}$ |
---|
372 | } |
---|
373 | \end{cfa} |
---|
374 | The local version of @?<?@ performs @?>?@ overriding the built-in @?<?@ so it is passed to @qsort@. |
---|
375 | Therefore, programmers can easily form local environments, adding and modifying appropriate functions, to maximize the reuse of other existing functions and types. |
---|
376 | |
---|
377 | To reduce duplication, it is possible to distribute a group of @forall@ (and storage-class qualifiers) over functions/types, such that each block declaration is prefixed by the group (see the example in Appendix~\ref{s:CforallStack}). |
---|
378 | \begin{cfa} |
---|
379 | forall( @T@ ) { $\C{// distribution block, add forall qualifier to declarations}$ |
---|
380 | struct stack { stack_node(@T@) * head; }; $\C{// generic type}$ |
---|
381 | inline { $\C{// nested distribution block, add forall/inline to declarations}$ |
---|
382 | void push( stack(@T@) & s, @T@ value ) ... $\C{// generic operations}$ |
---|
383 | } |
---|
384 | } |
---|
385 | \end{cfa} |
---|
386 | |
---|
387 | |
---|
388 | \subsection{Traits} |
---|
389 | |
---|
390 | \CFA provides \newterm{traits} to name a group of type assertions, where the trait name allows specifying the same set of assertions in multiple locations, preventing repetition mistakes at each function declaration. |
---|
391 | \begin{cquote} |
---|
392 | \lstDeleteShortInline@% |
---|
393 | \begin{tabular}{@{}l@{\hspace{\parindentlnth}}|@{\hspace{\parindentlnth}}l@{}} |
---|
394 | \begin{cfa} |
---|
395 | trait @sumable@( T ) { |
---|
396 | void @?{}@( T &, zero_t ); // 0 literal constructor |
---|
397 | T ?+?( T, T ); // assortment of additions |
---|
398 | T @?+=?@( T &, T ); |
---|
399 | T ++?( T & ); |
---|
400 | T ?++( T & ); |
---|
401 | }; |
---|
402 | \end{cfa} |
---|
403 | & |
---|
404 | \begin{cfa} |
---|
405 | forall( T @| sumable( T )@ ) // use trait |
---|
406 | T sum( T a[$\,$], size_t size ) { |
---|
407 | @T@ total = { @0@ }; // initialize by 0 constructor |
---|
408 | for ( size_t i = 0; i < size; i += 1 ) |
---|
409 | total @+=@ a[i]; // select appropriate + |
---|
410 | return total; |
---|
411 | } |
---|
412 | \end{cfa} |
---|
413 | \end{tabular} |
---|
414 | \lstMakeShortInline@% |
---|
415 | \end{cquote} |
---|
416 | |
---|
417 | Note that the @sumable@ trait does not include a copy constructor needed for the right side of @?+=?@ and return; |
---|
418 | it is provided by @otype@, which is syntactic sugar for the following trait. |
---|
419 | \begin{cfa} |
---|
420 | trait otype( T & | sized(T) ) { // sized is a pseudo-trait for types with known size and alignment |
---|
421 | void ?{}( T & ); $\C{// default constructor}$ |
---|
422 | void ?{}( T &, T ); $\C{// copy constructor}$ |
---|
423 | void ?=?( T &, T ); $\C{// assignment operator}$ |
---|
424 | void ^?{}( T & ); $\C{// destructor}$ |
---|
425 | }; |
---|
426 | \end{cfa} |
---|
427 | Given the information provided for an @otype@, variables of polymorphic type can be treated as if they were a complete type: stack allocatable, default or copy initialized, assigned, and deleted. |
---|
428 | |
---|
429 | In summation, the \CFA type system uses \newterm{nominal typing} for concrete types, matching with the C type system, and \newterm{structural typing} for polymorphic types. |
---|
430 | Hence, trait names play no part in type equivalence; |
---|
431 | the names are simply macros for a list of polymorphic assertions, which are expanded at usage sites. |
---|
432 | Nevertheless, trait names form a logical subtype hierarchy with @dtype@ at the top, where traits often contain overlapping assertions, \eg operator @+@. |
---|
433 | Traits are used like interfaces in Java or abstract base classes in \CC, but without the nominal inheritance relationships. |
---|
434 | Instead, each polymorphic function (or generic type) defines the structural type needed for its execution (polymorphic type key), and this key is fulfilled at each call site from the lexical environment, which is similar to the Go~\cite{Go} interfaces. |
---|
435 | Hence, new lexical scopes and nested functions are used extensively to create local subtypes, as in the @qsort@ example, without having to manage a nominal inheritance hierarchy. |
---|
436 | % (Nominal inheritance can be approximated with traits using marker variables or functions, as is done in Go.) |
---|
437 | |
---|
438 | % Nominal inheritance can be simulated with traits using marker variables or functions: |
---|
439 | % \begin{cfa} |
---|
440 | % trait nominal(T) { |
---|
441 | % T is_nominal; |
---|
442 | % }; |
---|
443 | % int is_nominal; $\C{// int now satisfies the nominal trait}$ |
---|
444 | % \end{cfa} |
---|
445 | % |
---|
446 | % Traits, however, are significantly more powerful than nominal-inheritance interfaces; most notably, traits may be used to declare a relationship \emph{among} multiple types, a property that may be difficult or impossible to represent in nominal-inheritance type systems: |
---|
447 | % \begin{cfa} |
---|
448 | % trait pointer_like(Ptr, El) { |
---|
449 | % lvalue El *?(Ptr); $\C{// Ptr can be dereferenced into a modifiable value of type El}$ |
---|
450 | % } |
---|
451 | % struct list { |
---|
452 | % int value; |
---|
453 | % list * next; $\C{// may omit "struct" on type names as in \CC}$ |
---|
454 | % }; |
---|
455 | % typedef list * list_iterator; |
---|
456 | % |
---|
457 | % lvalue int *?( list_iterator it ) { return it->value; } |
---|
458 | % \end{cfa} |
---|
459 | % In the example above, @(list_iterator, int)@ satisfies @pointer_like@ by the user-defined dereference function, and @(list_iterator, list)@ also satisfies @pointer_like@ by the built-in dereference operator for pointers. Given a declaration @list_iterator it@, @*it@ can be either an @int@ or a @list@, with the meaning disambiguated by context (\eg @int x = *it;@ interprets @*it@ as an @int@, while @(*it).value = 42;@ interprets @*it@ as a @list@). |
---|
460 | % While a nominal-inheritance system with associated types could model one of those two relationships by making @El@ an associated type of @Ptr@ in the @pointer_like@ implementation, few such systems could model both relationships simultaneously. |
---|
461 | |
---|
462 | |
---|
463 | \subsection{Generic Types} |
---|
464 | |
---|
465 | A significant shortcoming of standard C is the lack of reusable type-safe abstractions for generic data structures and algorithms. |
---|
466 | Broadly speaking, there are three approaches to implement abstract data structures in C. |
---|
467 | One approach is to write bespoke data structures for each context in which they are needed. |
---|
468 | While this approach is flexible and supports integration with the C type checker and tooling, it is also tedious and error prone, especially for more complex data structures. |
---|
469 | A second approach is to use @void *@-based polymorphism, \eg the C standard library functions @bsearch@ and @qsort@, which allow for the reuse of code with common functionality. |
---|
470 | However, basing all polymorphism on @void *@ eliminates the type checker's ability to ensure that argument types are properly matched, often requiring a number of extra function parameters, pointer indirection, and dynamic allocation that is otherwise not needed. |
---|
471 | A third approach to generic code is to use preprocessor macros, which does allow the generated code to be both generic and type checked, but errors may be difficult to interpret. |
---|
472 | Furthermore, writing and using preprocessor macros is unnatural and inflexible. |
---|
473 | |
---|
474 | \CC, Java, and other languages use \newterm{generic types} to produce type-safe abstract data types. |
---|
475 | \CFA generic types integrate efficiently and naturally with the existing polymorphic functions, while retaining backward compatibility with C and providing separate compilation. |
---|
476 | However, for known concrete parameters, the generic-type definition can be inlined, like \CC templates. |
---|
477 | |
---|
478 | A generic type can be declared by placing a @forall@ specifier on a @struct@ or @union@ declaration and instantiated using a parenthesized list of types after the type name. |
---|
479 | \begin{cquote} |
---|
480 | \lstDeleteShortInline@% |
---|
481 | \begin{tabular}{@{}l|@{\hspace{\parindentlnth}}l@{}} |
---|
482 | \begin{cfa} |
---|
483 | @forall( R, S )@ struct pair { |
---|
484 | R first; S second; |
---|
485 | }; |
---|
486 | @forall( T )@ // dynamic |
---|
487 | T value( pair(const char *, T) p ) { return p.second; } |
---|
488 | @forall( dtype F, T )@ // dtype-static (concrete) |
---|
489 | T value( pair(F *, T * ) p) { return *p.second; } |
---|
490 | \end{cfa} |
---|
491 | & |
---|
492 | \begin{cfa} |
---|
493 | pair(const char *, int) p = {"magic", 42}; // concrete |
---|
494 | int i = value( p ); |
---|
495 | pair(void *, int *) q = { 0, &p.second }; // concrete |
---|
496 | i = value( q ); |
---|
497 | double d = 1.0; |
---|
498 | pair(double *, double *) r = { &d, &d }; // concrete |
---|
499 | d = value( r ); |
---|
500 | \end{cfa} |
---|
501 | \end{tabular} |
---|
502 | \lstMakeShortInline@% |
---|
503 | \end{cquote} |
---|
504 | |
---|
505 | \CFA classifies generic types as either \newterm{concrete} or \newterm{dynamic}. |
---|
506 | Concrete types have a fixed memory layout regardless of type parameters, whereas dynamic types vary in memory layout depending on their type parameters. |
---|
507 | A \newterm{dtype-static} type has polymorphic parameters but is still concrete. |
---|
508 | Polymorphic pointers are an example of dtype-static types; |
---|
509 | given some type variable @T@, @T@ is a polymorphic type, as is @T *@, but @T *@ has a fixed size and can, therefore, be represented by @void *@ in code generation. |
---|
510 | |
---|
511 | \CFA generic types also allow checked argument constraints. |
---|
512 | For example, the following declaration of a sorted set type ensures the set key supports equality and relational comparison. |
---|
513 | \begin{cfa} |
---|
514 | forall( Key | { _Bool ?==?(Key, Key); _Bool ?<?(Key, Key); } ) struct sorted_set; |
---|
515 | \end{cfa} |
---|
516 | |
---|
517 | |
---|
518 | \subsection{Concrete generic types} |
---|
519 | |
---|
520 | The \CFA translator template expands concrete generic types into new structure types, affording maximal inlining. |
---|
521 | To enable interoperation among equivalent instantiations of a generic type, the translator saves the set of instantiations currently in scope and reuses the generated structure declarations where appropriate. |
---|
522 | A function declaration that accepts or returns a concrete generic type produces a declaration for the instantiated structure in the same scope, which all callers may reuse. |
---|
523 | For example, the concrete instantiation for @pair( const char *, int )@ is |
---|
524 | \begin{cfa} |
---|
525 | struct _pair_conc0 { |
---|
526 | const char * first; int second; |
---|
527 | }; |
---|
528 | \end{cfa} |
---|
529 | |
---|
530 | A concrete generic type with dtype-static parameters is also expanded to a structure type, but this type is used for all matching instantiations. |
---|
531 | In the above example, the @pair( F *, T * )@ parameter to @value@ is such a type; its expansion is below, and it is used as the type of the variables @q@ and @r@ as well, with casts for member access where appropriate. |
---|
532 | \begin{cfa} |
---|
533 | struct _pair_conc1 { |
---|
534 | void * first, * second; |
---|
535 | }; |
---|
536 | \end{cfa} |
---|
537 | |
---|
538 | |
---|
539 | \subsection{Dynamic generic types} |
---|
540 | |
---|
541 | Though \CFA implements concrete generic types efficiently, it also has a fully general system for dynamic generic types. |
---|
542 | As mentioned in Section~\ref{sec:poly-fns}, @otype@ function parameters (in fact, all @sized@ polymorphic parameters) come with implicit size and alignment parameters provided by the caller. |
---|
543 | Dynamic generic types also have an \newterm{offset array} containing structure-member offsets. |
---|
544 | A dynamic generic @union@ needs no such offset array, as all members are at offset 0, but size and alignment are still necessary. |
---|
545 | Access to members of a dynamic structure is provided at runtime via base displacement addressing |
---|
546 | % FIX |
---|
547 | using the structure pointer and the member offset (similar to the @offsetof@ macro), moving a compile-time offset calculation to runtime. |
---|
548 | |
---|
549 | The offset arrays are statically generated where possible. |
---|
550 | If a dynamic generic type is declared to be passed or returned by value from a polymorphic function, the translator can safely assume that the generic type is complete (\ie has a known layout) at any call site, and the offset array is passed from the caller; |
---|
551 | if the generic type is concrete at the call site, the elements of this offset array can even be statically generated using the C @offsetof@ macro. |
---|
552 | As an example, the body of the second @value@ function is implemented as |
---|
553 | \begin{cfa} |
---|
554 | _assign_T( _retval, p + _offsetof_pair[1] ); $\C{// return *p.second}$ |
---|
555 | \end{cfa} |
---|
556 | \newpage |
---|
557 | \noindent |
---|
558 | Here, @_assign_T@ is passed in as an implicit parameter from @T@, and takes two @T *@ (@void *@ in the generated code), a destination and a source, and @_retval@ is the pointer to a caller-allocated buffer for the return value, the usual \CFA method to handle dynamically sized return types. |
---|
559 | @_offsetof_pair@ is the offset array passed into @value@; |
---|
560 | this array is generated at the call site as |
---|
561 | \begin{cfa} |
---|
562 | size_t _offsetof_pair[] = { offsetof( _pair_conc0, first ), offsetof( _pair_conc0, second ) } |
---|
563 | \end{cfa} |
---|
564 | |
---|
565 | In some cases, the offset arrays cannot be statically generated. |
---|
566 | For instance, modularity is generally provided in C by including an opaque forward declaration of a structure and associated accessor and mutator functions in a header file, with the actual implementations in a separately compiled @.c@ file. |
---|
567 | \CFA supports this pattern for generic types, but the caller does not know the actual layout or size of the dynamic generic type and only holds it by a pointer. |
---|
568 | The \CFA translator automatically generates \newterm{layout functions} for cases where the size, alignment, and offset array of a generic struct cannot be passed into a function from that function's caller. |
---|
569 | These layout functions take as arguments pointers to size and alignment variables and a caller-allocated array of member offsets, as well as the size and alignment of all @sized@ parameters to the generic structure (un@sized@ parameters are forbidden from being used in a context that affects layout). |
---|
570 | Results of these layout functions are cached so that they are only computed once per type per function. %, as in the example below for @pair@. |
---|
571 | Layout functions also allow generic types to be used in a function definition without reflecting them in the function signature. |
---|
572 | For instance, a function that strips duplicate values from an unsorted @vector(T)@ likely has a pointer to the vector as its only explicit parameter, but uses some sort of @set(T)@ internally to test for duplicate values. |
---|
573 | This function could acquire the layout for @set(T)@ by calling its layout function with the layout of @T@ implicitly passed into the function. |
---|
574 | |
---|
575 | Whether a type is concrete, dtype-static, or dynamic is decided solely on the @forall@'s type parameters. |
---|
576 | This design allows opaque forward declarations of generic types, \eg @forall(T)@ @struct Box@ -- like in C, all uses of @Box(T)@ can be separately compiled, and callers from other translation units know the proper calling conventions to use. |
---|
577 | If the definition of a structure type is included in deciding whether a generic type is dynamic or concrete, some further types may be recognized as dtype-static (\eg @forall(T)@ @struct unique_ptr { T * p }@ does not depend on @T@ for its layout, but the existence of an @otype@ parameter means that it \emph{could}.); |
---|
578 | however, preserving separate compilation (and the associated C compatibility) in the existing design is judged to be an appropriate trade-off. |
---|
579 | |
---|
580 | |
---|
581 | \section{Contributions} |
---|
582 | |
---|
583 | |
---|
584 | |
---|
585 | \begin{comment} |
---|
586 | From: Andrew James Beach <ajbeach@uwaterloo.ca> |
---|
587 | To: Peter Buhr <pabuhr@uwaterloo.ca>, Michael Leslie Brooks <mlbrooks@uwaterloo.ca>, |
---|
588 | Fangren Yu <f37yu@uwaterloo.ca>, Jiada Liang <j82liang@uwaterloo.ca> |
---|
589 | Subject: Re: Haskell |
---|
590 | Date: Fri, 30 Aug 2024 16:09:06 +0000 |
---|
591 | |
---|
592 | Do you mean: |
---|
593 | |
---|
594 | one = 1 |
---|
595 | |
---|
596 | And then write a bunch of code that assumes it is an Int or Integer (which are roughly int and Int in Cforall) and then replace it with: |
---|
597 | |
---|
598 | one = 1.0 |
---|
599 | |
---|
600 | And have that crash? That is actually enough, for some reason Haskell is happy to narrow the type of the first literal (Num a => a) down to Integer but will not do the same for (Fractional a => a) and Rational (which is roughly Integer for real numbers). Possibly a compatibility thing since before Haskell had polymorphic literals. |
---|
601 | |
---|
602 | Now, writing even the first version will fire a -Wmissing-signatures warning, because it does appear to be understood that just from a documentation perspective, people want to know what types are being used. Now, if you have the original case and start updating the signatures (adding one :: Fractional a => a), you can eventually get into issues, for example: |
---|
603 | |
---|
604 | import Data.Array (Array, Ix, (!)) |
---|
605 | atOne :: (Ix a, Frational a) => Array a b -> b - - In CFA: forall(a | Ix(a) | Frational(a), b) b atOne(Array(a, b) const & array) |
---|
606 | atOne = (! one) |
---|
607 | |
---|
608 | Which compiles and is fine except for the slightly awkward fact that I don't know of any types that are both Ix and Fractional types. So you might never be able to find a way to actually use that function. If that is good enough you can reduce that to three lines and use it. |
---|
609 | |
---|
610 | Something that just occurred to me, after I did the above examples, is: Are there any classic examples in literature I could adapt to Haskell? |
---|
611 | |
---|
612 | Andrew |
---|
613 | |
---|
614 | PS, I think it is too obvious of a significant change to work as a good example but I did mock up the structure of what I am thinking you are thinking about with a function. If this helps here it is. |
---|
615 | |
---|
616 | doubleInt :: Int -> Int |
---|
617 | doubleInt x = x * 2 |
---|
618 | |
---|
619 | doubleStr :: String -> String |
---|
620 | doubleStr x = x ++ x |
---|
621 | |
---|
622 | -- Missing Signature |
---|
623 | action = doubleInt - replace with doubleStr |
---|
624 | |
---|
625 | main :: IO () |
---|
626 | main = print $ action 4 |
---|
627 | \end{comment} |
---|