But I'm talking about Capnproto schema language, not any particular
implementation. If catching mistakes were a viable goal, then there would
be no `AnyPointer`, since that is (and I quote documentation) "void* like
in C". Can't get any more explicit than that: `void*` catches no mistakes,
it's the idiom for type erasure, it lets you write type-unsound programs.
Same goes for generic interfaces and generic methods - no typechecking
there in the sense that either end of the RPC connection can use wrong
types and everything can then blow up, with no runtime diagnostics other
than when the pointer kinds mismatch.
As things are, it's impossible to divine when `AnyPointer` is usable and
when it isn't. The usual way the implementations deal with going from
`AnyPointer` to some specific type is by doing an explicit cast, just as
you would in C, letting the user assert that they know what they are doing.
I really fail to see what the Java snippet provided below has to do with
this Capnproto issue. In Java, `Object` is like `void*`, it's a
type-erasure type, and so are `object` and `dynamic` in C#. If you want to
do silly things, the runtime will stop you, and that's that, no problem -
you chose to use type erasure, you reap what you sow. In C, C++ and
Capnproto, the runtime won't generally stop you, and that's fine with me,
too. You can initialize whatever pointer type you want in an `AnyPointer`
field, and the user of the structure must somehow divine what really went
there.
So for there to be some coherency, either `AnyPointer` has to be removed,
or it has to be embraced to mean what it means in C/C++: type erasure and
delegation of type checking to the user, not the compiler. It's a broken
typecheck escape as it stands right now. If typechecks were not to be
bypassed, then not only would `AnyPointer` have to go, but interface
specializations would need to do typechecking on the wire, so that at least
at runtime the mistakes would be caught.
If `AnyPointer` is not a stand-in for `void*`, then a) this mention has to
go from the documentation, since neither design intent nor reference
implementation behavior are really like `void*`, and b) some guidance has
to be provided as to what purpose does `AnyPointer` serve, since clearly
generally taken type erasure isn't its goal. Except the few times it is, of
course. Sigh.
Looking past AnyPointer: You can specialize Capnproto generic interfaces to
any pointer type you want on either side of the wire, and so you can
specialize generic methods too, and it's entirely to the user and the
interface implementor to somehow agree on what types are really passed
around. There are no typesystem checks for this, and the parameter and
result wire formats can differ, etc. RPC typechecking is quite arbitrary in
general: the unspecialized interface type is typechecked on every method
call, as a consequence of an implementation detail (a sensible
implementation detail, I should add), yet the lower hanging fruit of
typechecking a generic interface's type parameters at capability
acquisition time is not done, even though its wire overhead would be
dwarfed by the cost of method invocations (including their inherent
typechecking).
Looking even further, this arbitrariness carries to constants, where you
can't do what is perfectly fine at runtime - and if one wasn't confused
already, this is a real stunner. At runtime you can e.g. initialize an
`AnyPointer` field as `Text`, but constants don't let you do that. I have
no idea in fact what was the intended behavior of an `AnyPointer` field in
a constant. This puzzles me to no end given that type soundness checking
lends itself well to compile-time typechecking of constants. If
`AnyPointer` is verboten where typechecking would be impossible, then
surely it should be allowed in constant context where typechecking can be
done. Imagine that in const context you replace every `AnyPointer` field
with a generic type parameter, and then substitute the actual types used,
and present the constant of such a type to the user: it's wire-compatible
with `AnyPointer`, and it's a minor matter to allow conversion from such a
specialized generic type to a non-generic type with `AnyPointer`
substituted for each generic parameter. Hey, the generic unspecialized type
already acts as if it had `AnyPointer` type parameters, so this is like 99%
done, the only missing step is conversion between `struct Generic(T) {
field @0 :T; }` and `struct NonGeneric { field @0 :AnyPointer; }` Maybe
`AnyPointer` should not be available, and instead one should be forced to
use type parameters wherever one would use `AnyPointer`? Would that be more
kosher? Or perhaps structs with `AnyPointer` fields should be transcribed
to generic types (effectively being some weird syntactic sugar), with
`AnyPointer` really meaning "an unnamed generic type parameter"?
The more I look into this, the more arbitrary it all seems to be, and I
can't visualize overarching design goals that might have driven this. Now I
do appreciate that implementation realities often curtail fully developed
designs, so I'm not trying to imply that the present way Capnproto works is
somehow inherently "bad" - it is what it is, and the only way to look is
forward, AFAICT.
Cheers, Kuba
On Tuesday, September 17, 2019 at 6:13:15 PM UTC-4, Ian Denhardt wrote:
>
> (Adding the list back to CC; I assume you didn't mean to just send this
> to me).
>
> > When passing GenericType to something that expects GenericType(Text),
> > it’s up to the user not to mess it up
>
> Catching this kind of mistake is the whole point of a type system. If
> you're going to make the argument that the type system shouldn't worry
> too much about edge cases and just act as a linter, then maybe you can
> claim that this isn't a big deal, but I think the premise that Kenton
> and I have been assuming is that type soundness (the property that
> well-typed programs do not have run-time type errors) is desirable here.
> Obviously this isn't really achievable for the C++ implementation
> overall since C++ itself fails this property, but it's probably worth
> hanging on to both for other languages and because getting closer to the
> goal in C++ is probably still a useful thing.
>
> ---
>
> To get into the details of what the problem is: perhaps this is review
> for everyone, but: the classic example of the problem with covariant
> generics and mutability is demonstrated by this java program:
>
> public class Main {
> public static void main(String[] args) {
> Integer[] ints = new Integer[4];
>
> // assign by reference, so `objs` points to the same array
> // as `ints`. Covariance (the notion that if A is a subtype
> // of B, then A[] is a subtype of B[]) is rule by which java
> // admits this statement:
> Object[] objs = ints;
>
> // And then because String is a subtype of Object, we can
> // put a string in our list of integers through `objs`:
> objs[0] = "OOPS";
> }
> }
>
> As Kenton suggests, the example critically depends on pointer aliasing
> for its unsoundness, so given that such aliasing is banned by the spec,
> it may not be possible to construct such an example in a given message.
> However, per my prior email it's not clear that you can't still run into
> trouble by aliased references to the root struct of a message from the
> rest of the program.
>
> -Ian
>
> Quoting Kuba Ober (2019-09-17 17:44:37)
> > Either I’m not getting something or this is certainly meant to work with
> mutable types?
> >
> > Any field accepting a GenericType should accept an arbitrary
> specialization, at least in the implementations I know of. Of course the
> application itself may further constrain what types are allowed, but we’re
> talking about static type checking within CapnProto runtime
> implementation(s).
> >
> > I consider implementations that would not allow it to be buggy –
> otherwise the entire premise of generic types in CapnProto is IMHO broken.
> As far as I can divine intent from CapnProto documentation, the generics
> were designed so that an unparametrized type is a stand-in for all of its
> specializations, both in co- and contravariant directions. When passing
> GenericType to something that expects GenericType(Text), it’s up to the
> user not to mess it up – there are several such areas in CapnProto where
> the sender of a message and the receiver must agree on what type is
> actually sent.
> >
> > It’s up to the implementer to make it possibly type-safe, e.g. an
> implementation could store the parameter type id and do a single check when
> GenericType.Reader is coerced to GenericType<Text>.Reader, and so on.
> >
> > Slightly confused, Kuba
> >
> > > 16 sep. 2019 kl. 6:31 em skrev Ian Denhardt <[email protected]
> <javascript:>>:
> > >
> > > Quoting 'Kenton Varda' via Cap'n Proto (2019-09-16 16:14:59)
> > >
> > >> Anyway, I guess given that there's no such thing as a constant
> > >> capability currently, we don't need to worry about that? And
> covariance
> > >> is correct for all other types? So we could support it?
> > >
> > > It's sound for constants, but given that it's not for mutable values
> > > (even without caps), my gut is that adding this is probably not a good
> > > cost:benefit ratio. It would only enable creating constants that would
> > > be impossible to construct dynamically anyway, and it's not clear to
> me
> > > what sort of programming this enables that justifies that.
> > >
> > > -Ian
>
--
You received this message because you are subscribed to the Google Groups
"Cap'n Proto" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/capnproto/29333b57-9383-4535-9949-e0ca0bff5c96%40googlegroups.com.