www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Duck typing and safety.

reply simendsjo <simen.endsjo pandavre.com> writes:
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.
Aug 13 2010
next sibling parent Mafi <mafi example.org> writes:
Am 13.08.2010 19:01, schrieb simendsjo:
 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.

something like this: void process(T)(T s) if ( __traits(hasMember, T, "shittyNameThatProbablyGetsRefactored")) {...} The if here is a template constraint, which is part of the signature (which means you can overload with those). If you try to instantiate your template in a wrong manner, you get a nice ct error at the instantiation. Then you can remove the unreachable 'war'-branch. Mafi
Aug 13 2010
prev sibling next sibling parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 13 Aug 2010 13:01:47 -0400, simendsjo <simen.endsjo 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
Aug 13 2010
parent simendsjo <simen.endsjo pandavre.com> writes:
On 13.08.2010 19:17, Steven Schveighoffer wrote:
 On Fri, 13 Aug 2010 13:01:47 -0400, simendsjo
 <simen.endsjo 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?
Aug 13 2010
prev sibling next sibling parent Adam Burton <adz21c googlemail.com> writes:
simendsjo 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.

can use constraints which I think would look something like this (not used D2 much at all yet so this is a guess). void process(T)(T s) if( __traits(hasMember, T, "shittyNameThatProbablyGetsRefactored")) { writeln("normal processing"); }
Aug 13 2010
prev sibling next sibling parent reply Ryan W Sims <rwsims gmail.com> writes:
On 8/13/10 10:01 AM, simendsjo 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.

If what you want is compile-time type safety, then duck typing is probably not for you. That's sort of the whole point behind duck typing: you defer typing decisions to runtime. If you need compiler breakage for something like that, use interfaces. -- rwsims
Aug 13 2010
next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 13 Aug 2010 13:20:32 -0400, Ryan W Sims <rwsims gmail.com> wrote:

 On 8/13/10 10:01 AM, simendsjo 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.

If what you want is compile-time type safety, then duck typing is probably not for you. That's sort of the whole point behind duck typing: you defer typing decisions to runtime. If you need compiler breakage for something like that, use interfaces.

No, duck typing is compile-time. Essentially, it goes like this: void foo(T)(T duck) { duck.quack(); } You can only call this functions on T types that can quack. If you try to call it on something else, the compiler refuses to build it. It's very similar to interfaces, except you don't have to declare what your interface is, you just pass in an object that can compile with the function, and you are done. Where it can get you into trouble is: 1. a function may have the same name and usage, but have a completely different meaning. Human languages are funny that way. This means, your function could accept a type as a parameter and use it in a very wrong way. Most of the time, this is a non issue, because you use duck typing with clear function names (hard to imagine another meaning for quack for instance). 2. The error might not be the function call, but in the type itself -- e.g. you spelled quack wrong. But the error does not appear as a problem with the type, just on its usage. Comparing this to interfaces, you declare to the compiler that your object implements a certain interface, and it errors when you don't. -Steve
Aug 13 2010
prev sibling parent "Simen kjaeraas" <simen.kjaras gmail.com> writes:
Steven Schveighoffer <schveiguy yahoo.com> wrote:

 1. a function may have the same name and usage, but have a completely  
 different meaning.  Human languages are funny that way.  This means,  
 your function could accept a type as a parameter and use it in a very  
 wrong way.  Most of the time, this is a non issue, because you use duck  
 typing with clear function names (hard to imagine another meaning for  
 quack for instance).

struct Charlatan { bool quack( ) { return true; } } -- Simen
Aug 14 2010
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 13 Aug 2010 13:32:11 -0400, simendsjo <simen.endsjo pandavre.com>  
wrote:

 On 13.08.2010 19:17, Steven Schveighoffer wrote:
 On Fri, 13 Aug 2010 13:01:47 -0400, simendsjo
 <simen.endsjo 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
Aug 13 2010
prev sibling parent Tomek =?UTF-8?B?U293acWEc2tp?= <just ask.me> writes:
simendsjo napisaƂ:

 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.

With template wizardry you can achieve an equivalent of interfaces for structs: struct Interface { bool foo(int, float); static void boo(float); ... } static assert (Implements!(S, Interface)); struct S { bool foo(int i, float f) { ... } static void boo(float f) { ... } ... } void process(T)(T s) if (Implements!(T, Interface)) { ... } And Implements!(T, I) is where the magic happens. It returns true if T has all the members of I with matching signatures. Not real interfaces, but close. Tomek
Aug 13 2010