[Prev][Next][Index][Thread]

Re: Dylan type-safety



In article <0T2m3.428$Fl.11872@typhoon-sf.snfc21.pbi.net>, "Harry Chomsky"
<harryc@chomsky.net> wrote:

> A discussion has come up in some other newsgroups (comp.lang.object a few
> weeks ago, comp.lang.eiffel yesterday) about whether Dylan has strong static
> type-checking.  My impression, as a relative newbie, is that it doesn't:
> even if you scrupulously declare the types of all your variables and
> methods, you can easily write code that causes run-time type errors with no
> warning from the compiler, and with no indication in the code that you're
> doing anything dangerous.
> 
> Of course, many people are perfectly happy with a dynamically-typed language
> such as Smalltalk, and if Dylan sees itself as another language in that
> category, I have no objection.  But Dylan programmers do seem to identify it
> as a strongly-typed language -- or rather, as an optionally strongly-typed
> language, in which you can prevent run-time type errors by following a
> certain coding discipline.  I'm not convinced that is the case.

A certain coding discipline, together with a cetain
quality-of-implementation of the compilers, I think.

Which we probably don't yet have :-)


> Here's a sample of some code that causes the kind of problem I'm talking
> about:
> 
>   define abstract class <bird> (<object>) end;
>   define class <sparrow> (<bird>) end;
>   define class <penguin> (<bird>) end;
> 
>   define generic fly (b :: <bird>) => ();
> 
>   define method fly (s :: <sparrow>) => ()
>     format-out("Sparrow flying!");
>   end method fly;
> 
>   // don't define fly (p :: <penguin>) -- penguins can't fly!
> 
>   define method get-bird() => (bird :: <bird>)
>     make(<penguin>);
>   end method get-bird;
> 
>   define function do-it()
>     let bird = get-bird();  // compiler doesn't know it's a penguin
>     fly(bird);              // causes run-time dispatch failure
>   end function do-it;
> 
> When I compile this sample with HD, in "production" mode, I don't get any
> warnings.  But it causes a run-time dispatch failure.

What do you get in Harlequin?

I think it's interesting that what you get in Gwydion is:

[bruce@hp birds]$ ./birds
Expected an instance of {the class <sparrow>}, but got {an instance of
<penguin>}
Aborted (core dumped)

This is interesting because it shows that the compiler knew quite a bit
about the situation.  In fact, here is the C code generated for do-it():

/* do-it{} */
descriptor_t * birdsZbirdsZdo_it_METH(descriptor_t *orig_sp)
{
    heapptr_t L_bird; /* bird */
    descriptor_t L_temp;

    /* #line {Class <unknown-source-location>} */

    #line 23 "./birds.dylan"
    /* get-bird{} */
    L_bird = birdsZbirdsZget_bird_METH(orig_sp);

    #line 365 "./condition.dylan"
    if ((birdsZbirdsZCLS_sparrow.heapptr == SLOT(L_bird, heapptr_t, 0))) {

        #line 24 "./birds.dylan"
        /* fly{<sparrow>} */
        birdsZbirdsZfly_METH(orig_sp, L_bird);
        return orig_sp + 0;
    }
    else {
        /* #line {Class <unknown-source-location>} */

        #line 368 "./condition.dylan"
        L_temp.heapptr = L_bird;
        L_temp.dataword.l = 0;
        /* type-error{<object>, <type>} */
        dylanZdylan_visceraZtype_error_METH(orig_sp, L_temp,
birdsZbirdsZCLS_sparrow.heapptr);
        not_reached();
    }
}


It would seem to be quite possible to have d2c issue a warning at compile
time that there could be a runtime error at this line -- after all it just
generated one in the code!

It would also be quite possible for d2c to issue an error or a warning
when it analyses the generic function for fly() and finds that there is no
applicable method for <penguin>.

d2c does neither of these things at the moment, but if it was desired it's
just a SMOP.


> Basically, the <penguin> class fails to live up to its contract. 

The problem is that declaring...

   define generic fly (b :: <bird>) => ();

... doesn't appear according to anything I can find in the DRM to be a
contract that all subclasses of <bird> will be able to respond to fly(). 
I agree that it would be very desirable to have this property, but:

a) Had fly() been an open generic (generics are sealed by default) it
would be impossible to ensure this at compile time because other libraries
could add fly(<penguin>).  It would also be impossible to ensure this at
link time, because methods could be added or removed from the generic
function at runtime.  However, given that the generic function is sealed,
the compiler could certainly check this, if it was part of the language.

b) a program that doesn't define fly(<penguin>) may be correct if it
happens to never call fly() on a <penguin>.  This is, of course,
impossible to prove at compile time.

c) a program that doesn't define fly(<penguin>) may still be correct even
if it calls fly() on a <penguin> because the program might trap and deal
with the "no applicable method" exception.

d) there would be an inconsistency in that a generic function that is not
declared explicity is implicitly the same as if it had been declared
taking parameters of type <object>.  It would be unreasonable to require
that every subclass of <object> should be able to respond to a call of
such a generic function.  We thus would need to make <object> an
exception.  Which seems sucky.


I'd sure like to see Dylan have (optionally) the sort of type-safety that
you want.

What I think I could support either:

1) a compiler switch that optionally causes the compiler to check that any
generic function for which at least one required parameter is declared to
be of a class other than <object> must have an applicable method for every
subclass of that class.  Where possible (sealed class and either sealed
generic or sealed domain) this is checked at compile time, otherwise it is
checked immediately at program startup, before main() is run.

Actually, due to add-method() and remove-method() this may not be possible
at program startup, but it could be redone at the first execution of the
generic method after add-method() or remove-method() are used.  Or, we
could just not do it at all for open generics.

I'm not sure whether this counts as a change to the language defnition or
not :-) but I think I'll put in on my wish-list for d2c.

OR

2) a new pair of keywords in the declaration of a generic method.  Perhaps
"covering" could be one of them, but I'm not sure what the opposite should
be.

Clearly this *is* a language change, and would need veeeery careful
consideration.


> Another way to describe the problem is that Dylan's types define
> implementations but not interfaces.

I'm not sure that is true.  An abstract class such as <table> pretty
clearly defines an interface rather than an implementation.

What Dylan doesn't do is guarantee that all aspects of the interface are
implemented.

Objective-C's "protocol"s and Java's "interface"s are better in that regard.

Since Dylan already has full multiple inheritence (unlike Obj-C and Java),
we have no use for a separate "protocol" concept, but it might be useful
to be able to specify the "covering" attribute for every method on a
particular (abstract?) class.  Hmmm ... maybe "protocol" and "adhoc" could
be the opposites, and could apply to either a class or to a generic
function?

Experts?

-- Bruce



References: