digitalmars.D.learn - Duck typing and safety.
- simendsjo (29/29) Aug 13 2010 While reading std.range, I though that a ducktyping design without
- Mafi (11/36) Aug 13 2010 As I understand this, you want t never start a nuclear war (lol) so do
- Steven Schveighoffer (23/53) Aug 13 2010 You have somewhat missed the point of duck typing. It would look more
- simendsjo (8/70) Aug 13 2010 Ok, point taken. But take a look at
- Steven Schveighoffer (24/104) Aug 13 2010 Well, one of the main differences between put and your simple example is...
- Adam Burton (9/47) Aug 13 2010 Put "static assert(0)" in the else condition for D1 or D2, or in D2 you
- Ryan W Sims (7/36) Aug 13 2010 If what you want is compile-time type safety, then duck typing is
- Steven Schveighoffer (24/67) Aug 13 2010 No, duck typing is compile-time. Essentially, it goes like this:
- Simen kjaeraas (8/14) Aug 14 2010 struct Charlatan {
- Tomek =?UTF-8?B?U293acWEc2tp?= (18/56) Aug 13 2010 With template wizardry you can achieve an equivalent of interfaces for s...
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
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.As I understand this, you want t never start a nuclear war (lol) so do 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
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
On 13.08.2010 19:17, Steven Schveighoffer wrote:On Fri, 13 Aug 2010 13:01:47 -0400, simendsjo <simen.endsjo pandavre.com> wrote: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?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
On Fri, 13 Aug 2010 13:32:11 -0400, simendsjo <simen.endsjo pandavre.com> wrote:On 13.08.2010 19:17, Steven Schveighoffer wrote: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. -SteveOn Fri, 13 Aug 2010 13:01:47 -0400, simendsjo <simen.endsjo pandavre.com> wrote: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?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
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.Put "static assert(0)" in the else condition for D1 or D2, or in D2 you 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
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
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: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. -SteveWhile 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.
Aug 13 2010
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
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