Duck typing and safety.

Steven Schveighoffer schveiguy at yahoo.com
Fri Aug 13 10:49:37 PDT 2010


On Fri, 13 Aug 2010 13:32:11 -0400, simendsjo <simen.endsjo at pandavre.com>  
wrote:

> On 13.08.2010 19:17, Steven Schveighoffer wrote:
>> On Fri, 13 Aug 2010 13:01:47 -0400, simendsjo
>> <simen.endsjo at pandavre.com> wrote:
>>
>>> While reading std.range, I though that a ducktyping design without
>>> language/library support can be quite fragile.
>>>
>>> Consider the following example:
>>>
>>> import std.stdio;
>>>
>>> struct S
>>> {
>>> void shittyNameThatProbablyGetsRefactored() { };
>>> }
>>>
>>> void process(T)(T s)
>>> {
>>> static if( __traits(hasMember, T,
>>> "shittyNameThatProbablyGetsRefactored"))
>>> {
>>> writeln("normal processing");
>>> }
>>> else
>>> {
>>> writeln("Start nuclear war!");
>>> }
>>> }
>>>
>>>
>>> void main()
>>> {
>>> S s;
>>> process(s);
>>> }
>>>
>>>
>>> If you rename S's method, process() does something completely
>>> different without a compile time error. By using interfaces this is
>>> avoided as the rename would break the interface.
>>>
>>> Is there any idoms you can use to avoid stuff like this? Relying on
>>> documentation doesn't seem like a good solution.
>>
>> You have somewhat missed the point of duck typing. It would look more
>> like this:
>>
>> void process(T)(T s)
>> {
>> s.shittyNameThatProbabyGetsRefactored();
>> }
>>
>>
>> Basically, the point is, you compile *expecting* that you can call the
>> function, and then when the type doesn't have the function, it simply
>> fails.
>>
>> Of course, the error you get is not what you want, because to the
>> compiler, it's not the call of the function that is the error, it's the
>> compiling of the function that is the error.
>>
>> To remedy this, you use template constraints:
>>
>> void process(T)(T s) if(__traits(hasMember, T,
>> "shittyNameThatProbabyGetsRefactored")
>> {
>> ...
>> }
>>
>> And then the compiler won't even try to compile the function, it just
>> fails at the call site.
>>
>> -Steve
>
> Ok, point taken. But take a look at
> void put(R, E)(ref R r, E e)
> in std.range for instance. This function uses a member put if it exists,  
> then front/popfront if it's an input range or opCall as a last instance.
>
> It's easy to imagine such a design for other types where suddenly the  
> program behaves differently because of a rename.
>
> Is "put" a bad design?

Well, one of the main differences between put and your simple example is,  
it still requires the type to have a certain interface.  In your example,  
you are not actually using the function you are testing for (in fact you  
are not using the object at all), so it's not an instance of duck typing.

But I agree that put can possibly get you into trouble if your type  
defines two methods that put uses, one of them being unrelated to put, and  
the higher precedence one goes away.

For example, you have a struct that has a put method and an opCall, but  
the opCall isn't used for output.  On some freak accident, the put member  
function gets renamed to output.  However, put(x) continues to compile  
because it now starts using the opCall.

There are two things we could do to fix this:

1. require that only ONE method be available.  So instead of put trying  
different methods in a certain order, it first verifies that it can only  
use one of the methods, and then uses that.
2. Rename put to reflect the method of output, like putOpcall, putRange,  
and put (which calls T.put).  This would be bad for generic programming,  
and is pretty much the whole point of put.

Other than that, it's impossible for the compiler to know what the user  
meant when he changed put to output, so the compiler really can't say  
much.  It is a legitimate gripe against duck typing.

-Steve


More information about the Digitalmars-d-learn mailing list