Baby Steps

A blog about programming, and tiny ways to improve it.

Declared vs Duckish Typing

One of the questions in our object system is what precisely how “declared” we want things to be when it comes to interfaces and implementations. In a discussion on IRC, graydon suggested it’d be nice to have terms like “duck-typing” defined more precisely in a Rust syntax, and he is correct. So here is my effort.

The current setup

Currently, implementations must declare precisely what types they implement. For example, it looks like this:

impl of draw for T {
    ...
}

where draw is an interface. Then, later, if we have an instance of type S and we wish to know whether it implementations the interface draw, we can scan through the set of implementations that are declared to implement draw and see if any of them are for the type S.

A more duck-typing like setup

Another option would be to remove the requirement that an impl declares what interfaces it implements. In that case, when we have a need to know if the type S implements the iface draw, we would again scan all of the implementations in scope for the type S. For each one, we would check whether it contains all the methods defined in draw. If so, we declare to be an implementation of the iface (we must also check that the methods contain the right types; it’s unclear to me whether we should do this check before or after deciding that it is an implementation, though).

Why duck typing?

It’s more convenient. There is also, currently, no good way to create an “after the fact” interface: support I have a bunch of types that all already have a draw() method and a bounds() method defined, and I’d like to make an iface like:

iface draw_and_bounds {
    fn draw();
    fn bounds();
}

and then just use it. Now everything just works. In the more statically declared world, I would then have to go over each type and do something like:

impl of draw_and_bounds for S { }
impl of draw_and_bounds for T { }

These impls just serve to declare that the type S (and T) implements the iface draw_and_bounds (and needs no additional methods to do so). Actually, this wouldn’t work today at all, because we don’t check for existing methods when deciding, so you’d really have to do something like:

impl of draw_and_bounds for S {
    fn draw() { self.some_other_impl::draw() }
    fn bounds() { self.some_other_impl::bounds() }
}

But of course the some_other_impl::draw syntax for naming a method isn’t implemented, so you’d have to do something like:

fn my_draw(self: S) { import some_other_impl; self.draw(); }
fn my_bounds(self: S) { import some_other_impl; self.draw(); }
impl of draw_and_bounds for S {
    fn draw() { my_draw(self) }
    fn bounds() { my_bounds(self) }
}

But we could fix that by implementing features.

Why not?

Just because methods with the right names are available doesn’t mean that they will do what you expect. Maybe you mean draw() as in “draw your gun” not “draw yourself on the screen”. It also prevents ‘marker interfaces’, like Java’s serializable.

Non-obvious implications and small design decisions

Simplicity and compilation time

One of the arguments for a non-duck-typing scenario is that it makes the system easier to implement. We can generate the vtable at the point of impl declaration and then refer to it from other places, rather than having to generate the vtable lazilly as needed.

It seems to me that it would affect compilation time. It’s bound to be faster to check compliance with the iface once, at the impl, then at each point of invocation. However, we can cache these results, so that’s probably not a big deal.

Frankenstein impls

A big open question (to me) is whether we should consider an interface to be implemented if all the necessary methods are available but they come from different sources. For example, consider something like:

impl draw for T { fn draw() { ... } }
impl bounds for T { fn bounds() { ... } }

Now, in a duck-typing world, is the draw_and_bounds iface implemented or not? It seems to involve a similar set of tradeoffs. If the answer is that they are not implemented, we need to write something explicit like impl draw_and_bounds for T { ... } just as we had to do when not using duck typing at all.

Still, I think that we should disallow such “frankenstein” impls. The main reason is that it makes instance coherence just about impossible to address (more on that in a later post, but in short form it prevents us from concisely naming the origin of the iface methods). It also makes the compiler more complex and heightens the danger of matching methods with the same names but different semantics.

This is a short-ish post. I’m sure there are many details I have omitted.

What do I want?

I don’t know. I originally wanted duck typing. Now I am somewhat undecided. I do think Frankenstein impls (something else I originally wanted) are bad (because of the instance coherence problems I alluded to). I think if we make the syntax for “reusing” existing methods to implement a new iface sufficiently compact, it’s probably not so painful. I am not really worried about semantic mismatches: these are rare in dynamically typed languages, and we have types and other checks that make such a mismatch unlikely.

Comments