New Issue: proposal for limited user-defined implicit conversions

16729, “mppf”, “proposal for limited user-defined implicit conversions”, “2020-11-17T19:02:29Z”

Related to #5054, #14213, #16576.

Background

We have been discussing to what extend user-defined implicit conversions are important for the language to support. We already have the ability to support copy-initialization and assignment between different types (with init= and = functions). In #16576, we asked if init= should cause implicit conversions to occur. At the same time, we are expecting not to allow potential cycles among implicit conversions. Since many types need to support = and init= in a way that creates cycles (e.g. assigning an array to a slice is OK and assigning a slice to an array is OK) that leads us away from using init= to enable implicit conversions.

Separately, some Chapel types have the property that storing them into a variable produces a different type. For example, var B = A[1..10] (i.e. creating a new variable storing a slice) will create a copy of the slice. This kind of pattern is useful for “view” structures. Issue #14213 has proposed that we enable a user-facing way to get this behavior for user-defined records.

Are user-defined implicit conversions needed?

Judging from experience C++ and Rust, user-defined implicit conversions are sometimes really important but should be used sparingly. From a language design point of view, that means that they should be supported (but potentially in a limited capacity).

  • The Google C++ style guide which indicates that implicit conversions should generally not be defined but that they can sometimes be necessary and appropriate for types that are designed to be interchangeable.

This is a similar sentiment to Scott Meyer’s More Effective C++, which indicates that implicit conversions should usually not be used but that they are sometimes essential.

In Rust, the implicit conversions can be used with .into. This syntax indicates that an implicit conversion can occur but does not indicate what the target of the conversion is. It does work for function calls.

See https://github.com/chapel-lang/chapel/issues/5054#issuecomment-726873643 for more details on these.

What use cases do implicit conversions need to support?

I surveyed use cases of implicit conversions that I am aware of in https://github.com/chapel-lang/chapel/issues/5054#issuecomment-728339302 . Some important questions I had in mind for that survey:

  • is it sufficient to only support implicit conversion to the inferred type (from #14213) ?
  • do the implicit conversions need to be able to return a reference ?
  • do the conversions occur within a new user-defined type, in to that new type, or out of that type?

From that survey, I think that the most important cases to support are:

  • implicit conversions to the inferred type
  • implicit conversions within a user-defined type

Proposal

So, here is a specific proposal. A record author can define two methods to indicate what implicit conversions are available.

  1. proc R.inferVariableType() type returns the type of x in var x = some-expression-of-type-R. Implicit conversions are allowed to this type. This supports #14213. When this method exists, the next method should also exist and implement the conversion.
  2. proc R.coerceToType(type t) returns the result of converting this to type t and uses a where clause / argument declarations like type t: int to limit the destination types allowed. This supports the remaining cases - most importantly coercions within a type (which is necessary for a unit library, or for coercions between range values of different integer widths).

It would not be possible to define these methods outside of the module (because these implicit conversions are considered part of a type rather something that can be bolted on later). Further, the compiler would detect cycles in these conversions and issue an error in that event. We might want it to also only allow 1 such user-defined implicit conversion in an implicit conversion sequence.

This proposal covers the use cases that I am aware of with the exception of int to bigint implicit conversions and range to domain implicit conversions. Implicit conversions from int to bigint do not seem to be required for bigint and much of the patterns are already supported by = and init= between the types as well as operators across the different types e.g. proc +( a: bigint, b: int). I don’t think that implicit conversions from range to domain should be in the language - I think these might be surprising and don’t seem necessary to me.

(TODO: make a separate issue about init= enabling casts).