Or structural type systems for the pendantic, but I think most people know what I mean when I say “compile-time duck typing”.
For one reason or another I’ve read quite a few blog posts about how great the Go programming language is recently. A common refrain is that Go’s interfaces are amazing because you don’t have to declare that a type has to satisfy an interface; it just does if its structure matches (hence structural typing). I’m not sold on how great this actually is – more on that later.
What I don’t understand is how this is presented as novel and never done before. I present to you a language from 1990:
template <typename T> void fun(const T& animal) { cout << "It says: " << animal.say() << endl; } struct Dog { std::string say() const { return "woof"; } }; struct Cat { std::string say() const { return "meow"; } }; int main() { fun(Dog()); fun(Cat()); }
Most people would recognise that as being C++. If you didn’t, well… it’s C++. I stayed away from post-C++11 on purpose (i.e. Dog{}
instead of Dog()
). Look ma, compile-time duck typing in the 1990s! Who’d’ve thunk it?
Is it nicer in Go? In my opinion, yes. Defining an interface and saying a function only takes objects that conform to that interface is a good thing, and a lot better than the situation in C++ (even with std::enable_if
and std::void_t
). But it’s easy enough to do that in D (template contraints), Haskell (typeclasses), and Rust (traits), to name the languages that do something similar that I’m more familiar with.
But in D and C++, there’s currently no way to state that your type satisfies what you need it to due to an algorithm function requiring it (such as having a member function called “say” in the silly example above) and get compiler errors telling you why it didn’t satisfy it (such as mispelling “say” as “sey”). C++, at some point in the future, will get concepts exactly to alleviate this. In D, I wrote a library to do it. Traits and typeclasses are definitely better, but in my point of view it’s good to be able to state that a type does indeed “look like” what it needs to do to be used by certain functions. At least in D you can say static assert(isAnimal!MyType);
– you just don’t know why that assertion fails when it does. I guess in C++17 one could do something similar using std::void_t
. Is there an equivalent for Go? I hope a gopher enlightens me.
All in all I don’t get why this gets touted as something only Go has. It’s a similar story to “you can link statically”. I can do that in other languages as well. Even ones from the 90s.
I’m am not sure that I agree with your assertion:
“But in D and C++, there’s currently no way to state that your type satisfies what you need it to due to an algorithm function requiring it…”
Correct me if I’m wrong but this can be done pretty straight forwardly with template specializations and interfaces:
“`
import std.stdio: writeln;
interface Animal{
string say() const;
}
class Dog: Animal{
string say() const{ return “woof!”;}
}
class Cat: Animal{
string say() const{ return “meow!”;}
}
template fun(T: Animal)
{
void fun(T animal)
{
writeln(“It says: “, animal.say());
}
}
void main() {
fun(new Dog());
fun(new Cat());
}
“`
In this case anything that doesn’t subscribe to the interface gets turfed out. In addition correct me if I am wrong but is this not the basis of doing multiple dispatching in D?
I meant using structs – in D using classes and interfaces has a cost in terms of how they’re used. I’d never thought of using them for compile-time dispatch though! Interesting.
You are explicitly inheriting from the interface. You could just as easily re-write the function as below and your example still compiles (and gives good error messages if you re-write Cat’s say function to sey).
“`
void fun(Animal animal)
{
writeln(“It says: “, animal.say());
}
“`
I think what Atila is referring to is a case where the fun function can call Animal types without a class explicitly inheriting from Animal. For instance, suppose you re-write Cat and a version of fun as below. Now you get error messages that say that it can’t deduce the type properly.
“`
class Cat {
string say() const
{
return “meow!”;
}
}
void fun(T: Animal)(T animal)
{
writeln(“It says: “, animal.say());
}
p.s. to provide that it is a compile time dispatch, you can pragma print …
“`
template fun(T: Animal)
{
void fun(T animal)
{
pragma(msg, T.stringof);
writeln(“It says: “, animal.say());
}
}
“`
when the script compiles pragmas will show that Dog and Cat overloads of the fun functions are created.
Have you had a chance to review: https://github.com/atilaneves/concepts/issues/2
In addition, I think it would be helpful to have something like a subtypes.d that corresponds to implements.d. Implements can detect if a struct/class implements an interface or abstract class, but it doesn’t handle more general subtyping relationships. For instance, if a struct is a subtype of another struct, or if a struct subtypes something it alias this’es. While I have written a isSubTypeOf template (that I plan to submit to phobos), it doesn’t have the same capability as a concepts version.
Sorry, somehow I missed your github issue. Taking a look now.
structs don’t have subtypes in D. alias this doesn’t count.
In my reading of the D spec, there actually is very little discussion of subtypes. For instance, in the classes page it says “An AliasThis declaration names a member to subtype. ” But I haven’t really seen it much anywhere else. They don’t define it there either.
So I’m operating on wikipedia’s explanation
https://en.wikipedia.org/wiki/Subtyping
which is that “If S is a subtype of T, the subtyping relation is often written S <: T, to mean that any term of type S can be safely used in a context where a term of type T is expected."
This means that if struct A has all the members and member functions of struct B, then A is a subtype of B. Thus, if A alias this'es B, then it should be a subtype by definition.
My PR is here
https://github.com/dlang/phobos/pull/5700
I need to update it to add attributes to the unittests, but it's in pretty good shape other than that. I purposefully ignore the member functions from object so that a struct can be a subtype of a class, so I make a small exception there.
I’m not sure I understand the problem. You can fairly easily check whether a class has a member function with a given name in C++. https://dev.krzaq.cc/post/checking-whether-a-class-has-a-member-function-with-a-given-signature/
The issue is asserting that your type has that member function and having the compiler tell you _why_ when that assertion fails.