Skip to content

Implicit argument conversion

jckarter edited this page Nov 30, 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 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. Defining an input argument as both converting alongside forward/lvalue/rvalue should thus be an error. 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 const 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));

Discussion

Please add comments here.

A benefit of the as syntax is that it naturally fits along with : to limit the set of allowed input types:

  [N | inValues?(N, Int32, Int16, Int8)]
  foo(x: N as Int, y as String) {
      /* body of foo */
  }

However, the semantics of that would be a bit strange: N could not be available to the body of foo, since it isn't a parameter of the underlying principal overload.