> > I'm trying to understand your reasoning, here. What's > the > > problem with inheritance and static typing? > > Foo f = new Bar(); > > f isn't a Foo, well, it sort of is, but it might not act > much like a Foo, in fact, I may need to use it as a Bar if > it is one, so I'll have to interrogate it as to its type > and downcast. Downcasting ends up being inevitable in > non-trivial systems.
Really? If these are related by inheritance, there's no problem handling this situation in a statically typed language, such as Java:
class Foo { void f() {} }
class Bar extends Foo { }
Foo f=new Bar();
f.f(); // Fine
I may be missing your point, though... Could you maybe provide a code example showing the problem?
> > However, what we _are_ somewhat struggling with in C++ > - > > but where I see the same problem in dynamically typed > > languages - is the support for "duck typing" > > Does it not strike you as odd that template programming > has become so much more important/prevalent in C++ because > it provides duck typing?
I see that what I wrote could be misunderstood. I think "duck typing" is very useful; what I meant was that the problem is the lack of _support_ for it, in languages like C++, as well as in dynamically typed languages (typically).
The problem I speak of is that there's typically _no_ type checking at function boundaries, when "duck typing" is used. Therefore, any type-related problems may manifest themselves as incomprehensive error messages deep into the implementation (and in the case of a dynamically typed language, you may not get any error at all, unless you ensure that all possible cases are covered with tests. As Vesa pointed out in another posting, you may not be able to do that, with a finite number of tests). Therefore, C++ templates are both great because they enable "duck typing" (at compile time), while being infamous for incomprehensive error messages from their use.
As I said, the same problem exists in languages with no type declaration on functions: any errors in the types of passed values are only (if even then) reported from the function implementation, not at the call site, where the error actually is.
This problem may be eliminated with support for "concepts" (abstract requirements on types, such as a type supporting the "+" operator, and what type that operator returns, etc.), for which there exists proposals for C++, and where it already exists in languages like Haskell ("type classes") and ML.
Interestingly, in Heron, there's apparently such support for "duck typing" both at compile time and run time.
> In the end, it turns out that > duck typing is the more sensible model IFF you have a > runtime that can catch and allow you to recover from type > errors.
Or perhaps better, catch them at compile-time, as described above. And even better, catching the error at the point it exists.
> C++ tries to do it all at compile time, in the end this > never quite works and you'll end up downcasting somewhere > because the compiler's notion of type safety is too > limiting. Downcasting gives you wiggle room at the > expense of safety.
It's correct that if you have a variable declared as a pointer or reference to a type "Base", and it actually points or refers to an object of type "Derived" (a subclass of "Base"), then - if you need something that only "Derived"'s interface provides, you may need to downcast, to get it. However, two things:
1) If you need this quite a bit, you may want to reconsider your design, as you're apparetly working against the type system (casting), and there may be a better way of doing things. And,
2) It's a completely safe operation: Downcasting in C++ using dynamic_cast either succeeds or returns NULL, that you may check for, so there's no risk of undefined behaviour, as long as you check the result (it performs a run-time check, if necessary).
I was just being verbose about the difficult of making useful inferences out of incomplete information. I don't think that we disagree on the technical aspects.
> I mean dynamic typing, i.e. that type information is > associated with values and not variables.
There are various ways to simulate that in statically typed languages (e.g. see my note about Universal Type) and some statically typed languages actually provide primitives for that. (Sorry, no time to elaborate on that now.)
> /* Does it not strike you as odd that template programming > has become so much more important/prevalent in C++ because > it provides duck typing? In the end, it turns out that > duck typing is the more sensible model IFF you have a > runtime that can catch and allow you to recover from type > errors. > */ > > Instead, > C++ templates provide a way to do things like > std::sort(iter first, iter last, [class] comp) > (http://www.sgi.com/tech/stl/sort.html). That is, the > sort that takes a "compare" function/function-like object > as an argument.
I think Todd is correct in saying that C++ templates use duck typing, and you just described what it's about: :) "the sort that takes a "compare" function/function-like object as an argument". If it looks like a function, works like a function, ... :) it can be used like a function (even though it may be an object).
> This isn't the same as duck typing, in that passing policy > classes to C++ templates is a way of expressing conceptual > similarity (the ability to sort), compared to calling by > function signature (duck typing).
The _mechanism_ I think can be called duck typing (in C++ and other languages supporting this). I.e. if an object has the right kind of operations available, then it will be accepted by the type system. The actual type doesn't matter.
However, what you may be thinking of is that the semantics also needs to be right, and - given that it doesn't check the actual type - there's no guarantee for this, unless you have something like support for "concepts" that have been mentioned a few times. See for example this page (http://neoscientists.org/~tschwinger/boostdev/concept_traits/libs/concept_traits/doc/) for a library-approach to support for "concepts" in C++.
> There are various ways to simulate that in statically > typed languages (e.g. see my note about Universal Type) > and some statically typed languages actually provide > primitives for that. (Sorry, no time to elaborate on that > now.)
> <i>Instead, C++ templates provide a way to do things like > std::sort(iter first, iter last, [class] comp) > (http://www.sgi.com/tech/stl/sort.html). That is, the sort > that takes a "compare" function/function-like object as an > argument.</i> > > Gosh, that's powerful stuff. Hey, wait a minute, isn't > that what qsort() did in plain old C thousands of years > ago? :)
Yes, except for the type safety and efficiency. In other words: not at all. ;) Besides, you can only pass pointers to qsort; no other types are allowed, so you can't pass in an object, for example (so you can't have state). Therefore, qsort is very different, and the reason templates are hot, and qsort is not. :)
It's whole concept of "generic programming" that is missing in the qsort case.
> Really? If these are related by inheritance, there's no > problem handling this situation in a statically typed > language, such as Java:
And if they are not related by inheritance, then you can't substitute one for another, even if they happen to implement the same protocol. Makes protocol adaptation very tricky.
> I think > "duck typing" is very useful; what I meant was that the > problem is the lack of _support_ for it, in languages like > C++, as well as in dynamically typed languages
Dynamically typed languages usually only implement duck typing.
> The problem I speak of is that there's typically _no_ type > checking at function boundaries, when "duck typing" is > used.
Why should you check there? In a duck typing system, you are only in trouble when you ask something to quack that can't. This is the point of error. Not the function call boundary. Until then, all is just ducky (sorry, I couldn't resist).
> Therefore, any type-related problems may manifest > themselves as incomprehensive error messages deep into the > implementation (and in the case of a dynamically typed > language, you may not get any error at all, unless you > ensure that all possible cases are covered with tests.
I seldom find the errors caused by type errors cryptic. They are usually obvious and straight forward. I also find that we spend way too much time guarding against this error considering how often it actually occurs in practice.
> As I said, the same problem exists in languages with no > type declaration on functions: any errors in the types of > passed values are only (if even then) reported from the > function implementation, not at the call site, where the > error actually is.
Yes, but usually you get the stack. For instance, in Smalltalk, you get a walkback (debugger) on the program that usually shows you right where your error is. These problems are easy to find and fix.
> This problem may be eliminated with support for "concepts" > (abstract requirements on types, such as a type supporting > the "+" operator, and what type that operator returns, > etc.),
To me, this is just more evidence that duck typing is the saner system. There is a continual loosening of typing constraints going on in c++ and gradual build up of meta information being maintained to allow typing errors to be less severe. "concepts" sounds suspiciously like "traits" which has recently been added to Squeak Smalltalk. Several other languages are beginning to adopt them as well. Basically they are method bundles that can be adapted to arbitrary classes without hierarchical constraints.
> even better, catching the error at > the point it exists.
That's a fairly fuzzy concept I think.
> It's correct that if you have a variable declared as a > pointer or reference to a type "Base", and it actually > points or refers to an object of type "Derived" (a > subclass of "Base"), then - if you need something that > only "Derived"'s interface provides, you may need to > downcast, to get it. However, two things: > > 1) If you need this quite a bit, you may want to > reconsider your design, as you're apparetly working > against the type system (casting), and there may be a > better way of doing things. And,
Often I find this to be a design error/unexpected use pattern in module I'm using that I don't control. So it is still a problem (and an unfortunately frequent one at that).
> 2) It's a completely safe operation: Downcasting in C++ > using dynamic_cast either succeeds or returns NULL, that > you may check for, so there's no risk of undefined > behaviour, as long as you check the result (it performs a > run-time check, if necessary).
Its ugly and verbose.
A* a = new B(); ... B* b = dynamic_cast<B>(a); if(b) { b.boo(); } else { throw new TypeCastError("Expected B here"); }
vs
a.boo(); // throws if boo not implemented on a
What's the difference apart from the second one gets me out of a lot of typing.
> Gosh, that's powerful stuff. Hey, wait a minute, isn't > that what qsort() did in plain old C thousands of years > ago? :)
Not really.
You're right that qsort can take a pointer to a function. It cannot take a function-like object, and it cannot do anything inline. But that's nitpicking.
The big difference is that qsort can only work with arrays/pointers. It cannot work with other containers. std::sort also absracts out the container, and can work with std::list, std::deque, std::vector, glib's various containers, and pretty much any container out there.
So, perhaps the ability to sort according to new policy isn't new. Templates provide an ability to abstract out far more than was possible in the past. And that is their main purpose, although they are sometimes used to relax the typing system.
Don't get me wrong. I'm no fan of the ultra strong super duper type system available in Ada. But I also have never had the overriding need to put an area code into a variable that used to hold a web page ($web_doc = 92503). It seems to me that static typing can find many errors that the programmer has to watch for in dynamically-typed languages.
/* I actually think people use templates in c++ to eliminate the coupling between hierarchy/type and protocol. */
Yeah, people DO that, but that's not the main use of templates today. Take a look at www.boost.org. The boost.any library does exactly what you're talking about, but just about every other library does what I'm talking about.
/* [me]: The ability to pass policy through functions/function-like objects is very powerful, and not available in some other languages.
[response]: You mean like
array sort: [:x :y | x > y]
? Neither x nor y is typed. The code will work if x responds to > with a y argument. It won't if it doesn't. */
In the example, the variables x and y are not typed, but the values in them are. When both x and y hold comparable types, the above code works fine. But if x is a string and y a bool, the code will not work. Now tell a beginner why not.
And why do you have to explain this to a beginner? Because dynamically-typed languages permit you to do things like this:
> array sort: [:x :y | x > y] > > ? Neither x nor y is typed. The code will work if x > responds to > with a y argument. It won't if it doesn't. > */ > > In the example, the variables x and y are not typed, but > the values in them are.
Yep.
> When both x and y hold comparable > types, the above code works fine.
Yep, and note that it doesn't actually matter what x and y are as long as they compatibly implement >
> But if x is a string > and y a bool, the code will not work. Now tell a beginner > why not.
Because you can't compare apples to oranges? Why do you think this is hard? This is EASY for a beginner to understand. Its much easier to grok than filling their head with a bunch of type rules. People "get" types. They use them all the time. Telling someone he can't pound a nail with a banana doesn't much surprise him. Insisting he use a hammer or specialization thereof will surprise him (you can certainly pound a nail with a crescent wrench if you need to).
So, from a substitutability perspective, your strictly typed world is harder to comprehend for the beginner as it front loads a whole lot of "theory" that isn't very intuitive.
> I think Todd is correct in saying that C++ templates use > duck typing, and you just described what it's about: :)
I understand duck typing to be a pattern to make calling methods that have the same signature (i.e., return the same type, take the same number and kind of arguments) easy, even if the classes in question are not related to each other.
Perhaps my counterexample was poorly picked. However, std::swap relies on templates to do much more than what duck typing offers. AND, other uses of policy classes permit far more than that (http://www.boost.org/libs/concept_check/concept_check.htm).
> I understand duck typing to be a pattern to make calling > methods that have the same signature (i.e., return the > same type, take the same number and kind of arguments) > easy, even if the classes in question are not related to > each other. > > Perhaps my counterexample was poorly picked. However, > std::swap relies on templates to do much more than what > duck typing offers. AND, other uses of policy classes > permit far more than that > (http://www.boost.org/libs/concept_check/concept_check.htm)
After reading that, I get the idea that concept checking is basically a trick to validate some type against a set of operations at compile time by forcing instantiation of a type parameterized checking function. A bit like a unit test but at compile time. Sort of a poor man's compile time [object conformsToProtocol: someProtocol] to borrow a line from ObjectiveC.
That's fine, but what if the operation I'm performing only requires part of the protocol? I guess I have to create a special sub concept checker?
Seems like a lot of extra work for dubious benefit. I'd rather have a system that just runs and when something goes wrong it helps me diagnose the problem.
It seems to me that C++ has an over-reliance on compile time checking because of an unhealthy obsession with avoiding the cost of maintaining useful meta information. In the end, C++ ends up being ever more bloated as endless instantiation of templates produces dozens of copies of the same binary. For instance:
List<A*> List<B*> List<C*> etc, all of which are probably identical binary but have different copies just because they are different "types".
> Because you can't compare apples to oranges? Why do you > think this is hard?
Not necessarily that it's hard, but that you still have to explain types. In fact it is proponents of loosely-typed systems that claim typing is a hard concept. But you can't get around teaching it, you just have to say "oh, and you don't have any way to enforce it."
I say this because Perl was the first language I learned. I understood the dynamic type system pretty easily, until I started working on bigger projects and ran into trouble precisely because of the loose typing.
Then it dawned on me. When I create a variable named something like $area_code I usually only want to store area codes in it, and run area code operations on it. However, there is no way to enforce that in a dynamically-type language. Yes, Perl's OO facilities let me enforce it in the area code *type*, but I can't enforce keeping the variable that type. I can create a reference to an area code type and set it to 27217, but if I later say $area_code = "it was the best of times, it was the worst of times" Perl won't tell me anything about this.
I like Perl, but I know most programming mistakes, especially beginner's mistakes, can be traced to this.
/* This is EASY for a beginner to understand. Its much easier to grok than filling their head with a bunch of type rules. */
See, you just said it was hard.
/* So, from a substitutability perspective, your strictly typed world is harder to comprehend for the beginner as it front loads a whole lot of "theory" that isn't very intuitive. */
Why? In Perl I have to remember what type I put in the variable. I still have to remember the type rules, I just have a much harder time enforcing them.
/* After reading that, I get the idea that concept checking is basically a trick to validate some type against a set of operations at compile time by forcing instantiation of a type parameterized checking function. */
I'm doing a terrible job of defending C++ templates.
Yes, you're right, that's the point of concepts (as they appear now -- there's a serious proposal to add better language support for them). My point is that you can specialize templates based on classes much like concepts. For instance, a std::list acts a lot like an old tape drive -- if I want information halfway throught the file, I have to start at the beginning and keep asking for the next record until I get there. std::vector is random access, I can just ask for the record in question, and get it. If I want to write a function that, given a sequence, returns the fifth element, I would want it to call just ask for the fifth element in a vector (v[5]), but get the first element, keep calling next, and tell me the fifth one.
And I can do that by creating classes called, say, random_access and sequential_access. These classes can be completely empty. I just need some way of automatically getting them if given a list or vector (often something like std::vector::itor_type), so that get_fifth_element can find out the kind of iterator involved and call the right implementation. There are ways to get that info automatically for things like C arrays that don't keep their iterator properties inside the container.
That is not duck typing. And that is where C++ templates are heading.
/* That's fine, but what if the operation I'm performing only requires part of the protocol? I guess I have to create a special sub concept checker? */
That's what Stroustrup considers the beauty of C++ templates, but it's also the feature that causes the most head scratching for new template programmers. C++ templates are lazily instantiated. The compiler will only check the portions of the protocol that the template function/class uses. If the template function/class uses more of the protocol later, many people will get errors with code that has worked for years when they recompile.
/* Seems like a lot of extra work for dubious benefit. I'd rather have a system that just runs and when something goes wrong it helps me diagnose the problem. */
Hopefully I've been able to explain some of the benefits.
The value of static checking isn't that it finds any errors dynamic checking overlooks -- it's that it finds those errors at compile time instead of run time. That may not matter on personal projects, but it sure matters when the person running your software isn't a programmer of any kind.
To take an example from C++, I just got a call from a friend who got an error message from RealPlayer that it could not "call method on pure virtual class." That's a run-time error message that I understand, but what should my friend do about it?
/* In the end, C++ ends up being ever more bloated as endless instantiation of templates produces dozens of copies of the same binary. For instance: */
Your example has been adressed with partial template specialization. I think even Visual C++ is able to handle it. I know gcc is.
change "I would want it to call just ask for the fifth element in a vector (v[5]), but get the first element, keep calling next, and tell me the fifth one."
to
"I would want it to call just ask for the fifth element in a vector (v[5]), but get the first element *of* *a* *list*, *and* keep calling next until it could give me the fifth one."
Otherwise it won't make sense.
Flat View: This topic has 95 replies
on 7 pages
[
«
|
1234567
|
»
]