Hacker News new | ask | show | jobs
by KerrAvon 474 days ago
> The interface type in golang is much more powerful than Java’s similar construct because its definition is totally disconnected from the implementation and vice versa. We could even make each codec a ReadWriter and use it all around.

This paragraph completely derailed me — I’m not familiar with golang, but `interface` in Java is like `@protocol` in Objective-C — it defines an interface without an implementation for the class to implement, decoupling it entirely from the implementation. Seems to be exactly the same thing?

2 comments

The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front—any type that has methods of appropriate names and signatures is considered to implement the interface, even if its designers were not aware of the interface’s existence. (This involves a small bit of dynamism in the runtime; easily cached, though, as the set of methods of a given type and the set of all interfaces are both fixed by the time the program runs.) Whether that’s a good thing depends on your design sensibilities (ETA: nominal vs structural).
For those wishing Java had a similar feature, there's Manifold: https://github.com/manifold-systems/manifold/tree/master/man...

Manifold is a very interesting project that adds a lot of useful features to Java (operator overloading, extension classes, and a whole bunch more). I don't know if it's smart to use it in production code because you basically go from writing Java to writing Manifold, but I still think it's a fun project to experiment with.

http://manifold.systems/

> Manifold is a Java compiler plugin, its features include Metaprogramming, Properties, Extension Methods, Operator Overloading, Templates, a Preprocessor, and more.

Neat tool. It is like having a programmable compiler built into your language.

The funny thing about Java is that while its design is to be entirely nominally typed, the way it is implemented in the JVM is compatible with structural typing, but there are artificial limitations set to follow the intended design (though of course, if one were to disable these limitations then modeled type safety goes out of the window as Java was simply not designed to be used that way). One community which takes advantage of this fact is the Minecraft modding space, as it is the basis[1] of how modding platforms like Fabric work.

1: https://github.com/SpongePowered/Mixin/wiki/Introduction-to-...

>The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front.

Go can't declare adherence up front, and in my view that’s a problem. Most of the time, explicitly stating your intent is best, for both humans reading the code and tools analyzing it. That said, structural typing has its moments, like when you need type-safe bridging without extra boilerplate.

You can assert that your type implements an interface at compile time, though, e.g.

    var _ AssertedInterface = &MyType{}
One of the main uses for interfaces in Go is defining the contact for _your_ dependencies. Rather than saying your function takes a socket, if you only ever call Write(), you can take a Writer, or define another interface that is only the set of functions you need. This is far more powerful than declaring that your type implements an interface up front. It allows for things like e.g. multiple image libraries to implement your interface without knowing it, enabling your project to use them interchangeably. And as another commenter said, you can have the compiler verify your compliance with an interface with a simple (though admittedly odd looking) declaration.
> It allows for things like e.g. multiple image libraries to implement your interface without knowing it

That virtually never happens. Seriously, what would be the odds? It’s so much more usual to purposefully implement an interface (eg a small wrapper the writer thingy that has the expected interface) than to use something that happens to fit the expected interface by pure chance.

It’s not a structural vs nominal problem but other, typescript is structural but has the implements keyword so that the interface compliance is checked at declaration, not at the point of use. You don’t have to use it and it will work just like Go, but I found that in 99% of cases it’s what I want: the whole point of me writing this class is because I need an interface implementation, might as well enforce it at this point.

I don’t agree it’s a structural VS nominal difference. Typescript is structural, but it does have the “implements” keyword.

Which makes a million times more sense to me, because realistically when do you ever have a structure that usefully implements an interface without being aware of it?? The common use-case is to implement an existing interface (in which case might as well enforce adherence to the interface at declaration point), not to plug an implementation into an unrelated functionality that happens to expect the right interface.

TypeScript doesn't require a class to use it, though, because it's structurally typed. All that "implements Foo" in this example does is make sure that you get a type error on the definition of "One" if it doesn't have the members of "Foo".

If "Two" didn't have a "name: string" member, then the error would be on the call to "test".

    interface Foo {
        name: string
    }

    class One implements Foo {
        constructor(public name: string) {}
    }

    class Two {
        constructor(public name: string) {}
    }

    function test(thing: Foo): void {
        //...
    }

    test(new One('joe'));
    test(new Two('jane'));
GNU C++ once had this feature; it was called Signatures. It was removed, though.

A signature declaration resembled an abstract base class. The target class did not have to inherit the signature: just have functions with matching names and types.

The user of the class could cast a pointer to an instance of the class to a pointer to a compatible signature. Code not knowing anything about the class could indirectly call all the functions through the signature pointer.

Nowadays you can do that with concepts.
I don't see how concepts can emulate signatures to the full extent that the target object can be manipulated as if it conformed to an abstract base, without any wrapper object being required to handle it.

Without signatures, we have to use some kind of delegating shim which takes the virtual function calls, and calls the real object. It could be a smart pointer.

With signatures, we don't use smart pointers, just "pointer to signature" pointers. However, I suspect those pointers had to be fat! Because, surely, to delegate the signature function calls to the correct functions in the target object class, we need some vtable-like entity. The signatures feature must generate such a vtable-like table for every combination of signature and target class. But target object has no space reserved in it for that table pointer. The obvious solution is a two-word pointer which holds a pointer to the object, and a pointer to the signature dispatch table specific to the signature type and target object's class.

If we can use concepts to do this, with a smart pointer that ends up being two words (e.g. pointer to its own vtable, and a pointer to the target object), we have broken even in that regard.

Here is an example then, assuming you mean this kind of abstrations,

    #include <iostream>

    using namespace std;

    template <typename T>
    concept Speaker = requires (T t) {
        t.speak();
    };

    class Duck {
        public:
        void speak() const {
            cout << "quack";
        }
    };

    class Dog {
        public:
        void speak() const {
            cout << "auau";
        }
    };

    class Cat {
        public:
        void speak() const {
            cout << "miau";
        }
    };

    template<Speaker T>
    void speaking_animal(const T&  animal) {
        animal.speak();
        cout << "\n\n";
    }

    template<Speaker... T>
    void speaking_farm(const T&... animals) {
        auto space_adder = [&](auto creature) -> void {
            creature.speak();
            cout << " ";
        };
        (space_adder(animals), ...);
    }


    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        speaking_animal(duck);
        
        speaking_animal(dog);

        speaking_animal(cat);

        speaking_farm(duck, dog, cat);

    }

Live example, https://godbolt.org/z/vPhf13xEh
Right, but speaking_animal(x) is not dynamic OOP dispatch; it's a template function that gets instantiated for each animal type.

Moreover, everything here can be done without a concept.

This version of the code builds with g++ -std=c++17. We just get worse diagnostics if we try to use something as a Speaker which doesn't conform.

    #include <iostream>

    using namespace std;

    class Duck {
        public:
        void speak() const {
            cout << "quack";
        }
    };

    class Dog {
        public:
        void speak() const {
            cout << "auau";
        }
    };

    class Cat {
        public:
        void speak() const {
            cout << "miau";
        }
    };

    template<typename T>
    void speaking_animal(const T&  animal) {
        animal.speak();
        cout << "\n\n";
    }

    template<typename... T>
    void speaking_farm(const T&... animals) {
        auto space_adder = [&](auto creature) -> void {
            creature.speak();
            cout << " ";
        };
        (space_adder(animals), ...);
    }


    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        speaking_animal(duck);
        speaking_animal(dog);
        speaking_animal(cat);
        speaking_farm(duck, dog, cat);
    }
I was thinking about more something along these lines. But note the double indirection: we end up passing the smart pointer animal_pointer by reference.

We achieve the "signature thing" though in that we take these animal objects and effectively get them to to conform to the common animal_pointer abstract base without their cooperation.

    #include <iostream>

    using namespace std;

    class Duck {
    public:
        void speak() const { cout << "quack"; }
    };

    class Dog {
    public:
        void speak() const { cout << "auau"; }
    };

    class Cat {
    public:
        void speak() const { cout << "miau"; }
    };

    class animal_pointer {
    public:
        virtual void speak() const = 0;
    };

    template <typename T> class animal_pointer_impl : public animal_pointer {
    private:
        T *obj;
    public:
        animal_pointer_impl(T *o) : obj(o) { }
        virtual void speak() const { obj->speak(); }
    };

    void animal_api(const animal_pointer &p)
    {
        p.speak();
 cout << '\n';
    }

    int main() {
        Duck duck;
        Dog dog;
        Cat cat;

        animal_pointer_impl<Duck> p0(&duck);
        animal_pointer_impl<Dog> p1(&dog);
        animal_pointer_impl<Cat> p2(&cat);

        animal_api(p0);
        animal_api(p1);
        animal_api(p2);
    }
animal_api is a regular function, which represents some external API that we don't get to recompile.
So golang supports 'duck typing'?
I think in a static context, it's generally referred to as structural typing, but yeah.
interfaces in Go are structural. Interfaces in Java are nominal and require immediate declaration of intent to implement at type definition.
Shouldn’t this be named phenomenal rather than structural? In both cases there is a structure assumed, but one is implicitly inferred while the other one is explicitly required.
The difference is that in Go, an interface is assumed to match if the method signatures match. In other words, the match is done on the type structure of the interface, hence the “structural” designation. Nominal typing, on the other hand, considers that interfaces tend to be associated with important semantic requirements in addition to the type signature, and that mere type-structure matching doesn’t at all guarantee a semantic match. For that reason, the semantics are implicitly bound to the declared name of the interface, and the way for an implementation to claim conformance to those semantics is to explicitly bind itself to that name.
I think you’re making a joke, but in Go you get both. You can have the compiler enforce that you implement an interface with a simple declaration. Most people do.
No intended joke in that case, but it’s nice to have the feedback it might seen as if it was.

I don’t know Go to be frank, just had a very shallow look at it once because of an interview, and apart big names behind it, it didn’t shine in any obvious way — but that’s also maybe aligned with the "boring" tech label it seems associated with (that is, in positive manner for those who praise it).