Open DanilaFe opened 11 months ago
Would I be able to combine &
and |
in the same expression? For example, I could imagine wanting to define serializable
as writeSerializable & (readSerializable | initDeserializable)
. That is, "writeSerializable and at least one way to deserialize".
I didn't think it would be essential, but it's possible. This does place us straight into the "boolean satisfiability problem with interfaces", but that seems relatively benign.
This is an interesting proposal! I would like to think more about the |
handling, this part:
Though it was less important in the & case, the semantics of both of the new interface types I'm suggesting attempt to satisfy / implement every constraint in their definition: there is no short-circuiting. Thus, above, record D will implement valContextManager, refContextManager, and constRefContextManager.
feels off to me. But I think what you mean is that "if all contraints are fulfilled, then all constraints will be implementable"? So something like:
record E : contextManager {
proc enterContext() { ... }
proc enterContext() const ref { ... }
proc exitContext(in err: owned Error?) { ... }
}
would be usable with valContextManager
and constRefContextManager
in addition to contextManager
, but not refContextManager
? Which seems perfectly reasonable to me, barring any issue that we discover as part of implementing it
I guess what I mean is that if I = A | B | C
, and A
is satisfied, it doesn't give up trying to satisfy B
and C
. This way, the type can still be used for B
and C
, even though in terms of "or" evaluation, only A
is needed.
would be usable with valContextManager and constRefContextManager in addition to contextManager, but not refContextManager? Which seems perfectly reasonable to me, barring any issue that we discover as part of implementing it
Yup, that's what I envisioned.
interface serializable = readDeserializable & initDeserializable & writeSerializable;
I'm not so sure that this is the meaning of &
that I would expect. I think what you're going for here is that the interface is the union of all of these 3 RHS interfaces. But I would think of &
as meaning intersection; as in, only those methods/functions that appear in all 3 of the RHS interfaces would appear in the new interface.
My counter-proposal is to treat it instead as multiple inheritance of interfaces:
interface serializable : readDeserializable, initDeserializable, writeSerializable {
}
As far as I know, multiple "inheritance" of interfaces is relatively understandable. At least, Java allows this sort of thing (but, granted, Java interfaces are very different).
One thing I would like to know is how Swift handles this case.
For the |
interfaces and the context manager case, I think we should just treat it as a special case in the compiler (where it knows what record D : contextManager
means). Why do I think this?
contextManager
interface|
feature (at least as far as I know)|
support requires more language design and features than I'm comfortable designing with just this one caseI think we should drag our feet on creating a way for users to define an interface like contextManager
until we have a motivating case.
Background
We have introduced interfaces in 1.32 as "the way" to mark a type as implementing certain special methods. However, interfaces as they are in Chapel today are not sufficient to properly support the language features that rely on them. There are two such cases:
Serializers / deserializers: For these, we have settled on a "parent interface",
serializable,
that is composed of three other interfaces,initDeserializable
,readDeserializable
, andwriteSerializable
. Currently, interfaces can't be "combined" into a bigger interface in this way.Context managers: Context managers rely heavily on the return intents of the
enterContext
functions to decide which (out of potentially several), should be called. For instance, consider the following code:A different
enterContext
function will be called depending on the form of themanage
statement.The user can define any of the 7 combinations of
enterContext
functions and still keep their type "suited" for amanage
statement (though not necessarily any manage statement -- if a user provides only aconst ref
overload, they won't be able to use amanage as ref
statement).Defining an interface for this is not currently possible, because interfaces expect a consistent list of functions that each type needs to implement. We would only be able to require a particular combination of overloads (one of the seven possible). However, since each of the functions does have a fixed signature, it seems like what we really want is to define a separate interface for each:
Proposed Solution:
&
and|
interfaces'&' interfaces
My proposed solution flows naturally out of the need to define
serializable
as an interface that combines the three possible sub-interfaces into one. One might intuitively attempt to define such an interface as follows:The above definition of serializable works for method and function signatures: if a type implements serializable, our constraint generic functions already do the work to expose the functionality provided by nested
implements
statements. However, we run into trouble trying to implement the interface.We could work around this with some compiler assistance, of course, and achieve our results. But then, the solution is a bit unsatisfying: we have a fairly verbose interface definition for
serializable
, and we have compiler magic to support it, meaning users are not able to implement such "combination interfaces" themselves. I propose extending the language with a '&' interface that serves a similar purpose:Notionally, I'd expect writing
serializable
to be "just like" simply writing the three interfaces that make it up. Thus, the following two lines would be (notionally) equivalent.This has the following advantages:
'|' interfaces
A curious thing to do when faced with something that kind of looks algebraic, like our
&
, is to see about its dual. The dual of&
is|
, and it's exactly this sort of interface that will help the problem with context managers. Instead of requiring the user to implement every interface, it requires only a single interface:Note the uses of
&
and|
instead of&&
and||
for these interfaces: this is intentional. Though it was less important in the&
case, the semantics of both of the new interface types I'm suggesting attempt to satisfy / implement every constraint in their definition: there is no short-circuiting. Thus, above, recordD
will implementvalContextManager
,refContextManager
, andconstRefContextManager
. This seems like the most reasonable approach, since the type clearly meets the constraints of all three interfaces, and thus should be usable in constraint generic functions that accept onlyconstRefContextManager
orrefContextManager
types.It should be noted that the semantics of resolving constraint-generic functions are quite different from those of resolving regular generic functions: a constraint generic function is resolved only once, with interface-provided functions being resolved to their interface definitions (e.g. a call to
foo
will be resolved to aSelf.foo()
definition in an interface). Then, when a constraint generic function is resolved, the calls toSelf.foo()
are replaced with the witnesses from implementing the given interface. We skip function resolution, as well as return intent overloading.With that in mind, how can we resolve a function that has
A | B
as an argument constraint? Since we don't know for sure whetherA
orB
is implemented, I propose we opt for assuming neither. In this way, a formal with no known interfaces and a formal with a disjunction interface will be equivalent.For context managers, that's enough -- compiler support can be used to figure out the rest. However, it would obviously be unsatisfying to introduce a language feature like this only for the sake of context managers, and make it unusable without compiler magic. There are ways to use
|
-interfaces from a user perspective: they just require additional syntax and implementation work, so I propose leaving them out for an initial prototype. That said, I envision a form ofif
-style syntax working here.This would complicate the resolution process of constraint-generic functions somewhat (which currently do not require
param
-resolution after instantiation). However, I think this works quite well with the existing semantics of the "implements expression"x.type implements A
, which already occurs in where clauses in constraint generic function signatures:Finally, I think this interface presents a way to implement a relatively long-standing desire for users to implement their own typeclasses such as 'numeric' using disjunction. It doesn't quite do it out of the box (you can't use
int(64)
in an interface disjunction), but I can see it being useful in that way.What it will all look like
If both conjunction (
&
) and disjunction (|
) interfaces are implemented, user code will not have to change: types implementingserializable
andcontextManager
will continue to work as usual. Library support code will look something like this: