Skip to content

Implicit argument conversion

jckarter edited this page Nov 13, 2010 · 7 revisions

Motivation

Pervasive ad-hoc implicit type conversion, as in C++, is undesirable and leads to difficult interactions with generic programming features. However, many functions don't require genericity and naturally operate only on a single set of input types, and for these functions, converting similar input types automatically on the caller's behalf is convenient and desirable. Additionally, many C APIs have poor type discipline and use disparate integer types with the assumption that the language will implicitly convert between them. Dealing with these APIs in Clay is currently difficult without manually writing wrapper code to handle the conversions taken for granted by the raw C API.

Implicit conversion can currently be emulated in Clay using a "funnel" pattern, where a function has one or more generic overloads that convert their inputs to a set of canonical types, which are then fed into the principal overload to perform the actual work:

// Generic overload
[I, S | Integer?(I) and String?(S)]
foo(x: I, y: S) = forward ...foo(Int(x), String(y));

// Principal overload
overload foo(x: Int, y: String) {
    /* body of foo */
}

This is a common enough pattern that the language should provide syntax to eliminate the boilerplate of the pattern. Also, the relationship between types and type class predicates (Int and Integer?, String and String?, etc.) is currently ad-hoc, and constructors provide a poor mechanism for implicit conversion, as they are often more general than implicit conversions should be (for example, Int("5")) and copy construction is not desirable in the case an argument is already the proper type. To better control implicit conversion behavior, the library should provide a formal conversion protocol separate from construction and type predicates. The call operation for fixed-input-type *CodePointer and Lambda types, as well as for Clay functions with arguments declared implicitly convertible, should then use this conversion protocol.

Proposed implementation

a separate "convert" function should be provided, along with a Convertible? predicate for type patterns:

// [T, U] convert(x: T, static U) U
procedure convert;
// don't copy if the value is already the correct type
[T] overload convert(x: T, static T) = ref x;
// implicitly convert to larger integer types
[I, J | Integer?(I) and Integer?(J) and BiggerInteger?(J, I)] overload convert(x: I, J) = J(x);
// etc.

Convertible?(From, To) = CallDefined?(convert, From, Static[To]);

There are a number of possible approaches for implicitly convertible argument syntax. One possibility is a new "converting" keyword:

foo(converting x: Int, converting y: String) {
    /* body of foo */
}

Another possibility is to reuse the "as" keyword, using it to replace the colon for implicitly convertible arguments:

foo(x as Int, y as String) {
    /* body of foo */
}

If we take that approach, it may also be desirable to allow "as" as operator syntax for convert(), similar to C#'s "as" syntax.

Regardless of the syntax, a definition using implicit conversion should desugar into a pair of overloads similar to the motivating example, using the convert() protocol:

// desugared result of the above syntax

[TX, TY | Convertible?(TX, Int) and Convertible?(TY, String)]
inline foo(forward x: TX, forward y: TY) = ...forward foo(convert(x, Int), convert(x, String));

foo(/*const*/ x: Int, /*const*/ y: String) {
    /* body of foo */
}

Note that implicit conversion is incompatible with "forward", "lvalue", or "rvalue" because the implicit conversion may or may not generate an rvalue separate from the original argument. The body of foo() can only reliably treat its arguments as constant. When clay gets "const" input arguments, the inputs to the principal overload should be implicitly indicated as such.

For fixed-argument-type callable objects, such as *CodePointer and Lambda, all of the arguments to their associated call operation could be implicitly converted:

[...I, ...O, ...T | ConvertibleTypes?((...I), ...T)
overload call(cp: CCodePointer[(...I), (...O)], forward ...args: T) = forward ...callCCodePointer(...convertValues((...I), ...args));