Duck typing (horror) in Go
ccampo.me
ccampo.me
// WriteTo implements io.WriterTo.
// This may make multiple calls to the [Reader.Read] method of the underlying [Reader].
// If the underlying reader supports the [Reader.WriteTo] method,
// this calls the underlying [Reader.WriteTo] without buffering.
func (b *Reader) WriteTo(w io.Writer) (n int64, err error) {
b.lastByte = -1
b.lastRuneSize = -1
n, err = b.writeBuf(w)
if err != nil {
return
}
if r, ok := b.rd.(io.WriterTo); ok {
m, err := r.WriteTo(w)
n += m
return n, err
}
if w, ok := w.(io.ReaderFrom); ok {
m, err := w.ReadFrom(b.rd)
n += m
return n, err
}
if b.w-b.r < len(b.buf) {
b.fill() // buffer not full
}
for b.r < b.w {
// b.r < b.w => buffer is not empty
m, err := b.writeBuf(w)
n += m
if err != nil {
return n, err
}
b.fill() // buffer is empty
}
if b.err == io.EOF {
b.err = nil
}
return n, b.readErr()
} if quacker, ok := duck.(interface { Quack() }); ok {
quacker.Quack()
}
Ignoring errors leads to unexpected results.Failing to mention that it's purposely ignoring an error for a fun blog post is the opposite of informative.
Go also has an 'unsafe' package that can mess things up at runtime--don't do that and don't ignore errors.
An example is in the errors package: https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/...
https://github.com/golang/go/blob/master/src/cmd/go/internal... https://github.com/golang/go/blob/master/src/io/fs/fs.go#L26... https://github.com/golang/go/blob/master/src/errors/wrap.go#... https://github.com/golang/go/blob/master/src/go/parser/resol...
If `ok' gets set to false, that seems optimal. Pretty neat, in fact, what other language permits such low-key, easy runtime interface checks?
v := hello.(world)
this never panic:
v, ok := hello.(world)
and yes, its awesome. people rag on Go because BLARG ERRORS BOILERPLATE, but if you can get over that Go is not Python/JavaScript, you're left with a simple powerful language
Nominal typing means use of an object type checks if the explicitly named type of the object matches the explicitly named type the operation support (modulo some type inference). For example, C is nominally typed.
Duck typing means that use of an object type checks if all operations on the object are defined, without that set of operations ever being explicitly defined. For example, C++ template substitution uses duck typing (pre-concepts).
Finally, structural typing means that use of an object type checks if some explicitly specified set of operations on it are defined. For example C++20 templates with constraints use structural typing. Go interfaces seem to as well, although I'm not familiar enough with Go to know if that's exactly true.
Can you please share an example? Structural typing[1] sounds like a subset/special case of Duck typing.
[1] https://www.typescriptlang.org/docs/handbook/type-compatibil...
template<typename T> concept CanDouble = requires(T t) {t + t; t * 2;};
template<CanDouble F> auto foo(F t) {return t + t;}
template<typename B> auto bar(B t) {return t + t;}
As I see it, the foo template function uses structural typing, because the required set of properties of F are explicitly specified (in this case with name "CanDouble"). Two operations, t + t and t * 2, must be defined for F. On the other hand, bar uses duck typing, because B can be any type that can be added to itself.Note that in this example, foo(std::string{"abc"}) will not compile, because string doesn't satisfy all the properties of CanDouble, even though the property it doesn't satisfy isn't actually used by foo. In other words, strings don't have the structure described by CanDouble. However, bar(std::string{"abc"}) does compile, because strings can be added to strings in C++, and B is only restricted by how it's used in bar. In other words strings "walk like a duck", so they're a duck.
Isn't that still duck typing? If the type "walks like a duck" with respect to those two operations, then the concept will automatically be satisfied, even if the type knows nothing about the concept. Or, if I create two different concepts with the same requirements:
template<typename T> concept CanDouble = requires(T t) { t + t; t * 2; };
template<typename T> concept CanTrouble = requires(T t) { t + t; t * 2; };
then they'll be satisified by exactly the same set of types: you can't satisfy one without satisfying the other. So I don't think there's really much 'structure' in either case, the concept is really just acting as an alias for the duck-properties. For structural interfaces in C++, you need base classes or specialization or other such constructs.It's more an illustration that with Go you can, if you choose, take "any" as your argument type and then later cast to the actual type you need, risking, of course, run-time exceptions if the type is not fulfilled.
Basically, you can opt-in to dynamic typing (the same as e.g. C#).
If the method quack were
quack(ducks ...Duck)
then you'd get a compile-time error.Not cast to just a concrete type, but to an interface. Literally duck typing.
> then you'd get a compile-time error.
Which is emblematic of structural typing, and the only real difference between structural typing and duck typing is that one is dealt with at compile time and the other at runtime. The article tells that Go supports both.
Yes -- supporting structural typing + also runtime casts means you support duck typing.
That's not duck typing, though. It's similar but not the same.
With duck typing there isn't an interface to cast to, but the runtime object has a method matching the one being called. If you're casting, then it's not real duck typing.
People often imagine a "metaclass" or "meta-interface" for each method to help understand how duck typing works, but it's just a mental model to understand the concept. In reality there is no interface tying the methods together, and the whole thing is done at runtime, along with a runtime error if the method doesn't exist.
'If' being the operative word. You're not actually casting. That was merely to not try and confuse the commenter further. In fact, Go doesn't even support casting! It does support type conversion, which is oft confused with casting, but that uses the syntax `type(variable)`, which is not what is going on here either.
> In reality there is no interface tying the methods together, and the whole thing is done at runtime, along with a runtime error if the method doesn't exist.
Right, which is how it works.
public interface ISwim { void Swim(); }
public interface IQuack { void Quack(); }
public interface IDuck extends ISwim, IQuack { }
public class Mallard implements IDuck {
public void Swim() { System.out.println("mallard swimming"); }
public void Quack() { System.out.println("mallard quacking"); }
}
public class Dog implements ISwim {
public void Swim() { System.out.println("dog swimming"); }
public void Bark() { System.out.println("dog barking"); }
}
public static void quack(Object... ducks) {
for (Object duck: ducks) {
((ISwim)duck).Swim();
((IQuack)duck).Quack();
}
}
public static void main() {
quack(new Mallard(), new Dog());
}
in Java? You can do this in Java, you know, casting down to an interface; the same goes for C#. Do those languages now have duck typing, just because in them you can up-cast anything to Object, and then try to down-cast to any other type, be it an interface or a class?Maybe. I don't have a language reference handy. Does runtime type checking take place with that code?
My impression, based on the way you said it (and my vague memories of using Java once upon a time), is no. On that assumption, where would you find the duck type? The significance of both structural typing and duck typing is that type checking occurs. As before, the difference is when the check takes place – be it at compile time or at runtime.
Perhaps you could update that code to demonstrate how you would bypass calling objects which, when type checked, show to not quack like a duck like as is shown in the Go examples?
And no, the structural/duck typing is not about when the type checking happens. It's about not having to write "implements IDuck" right in the class declaration for the down-cast to succeed.
That doesn't make sense. Not having to write "implements IDuck" is true of both structural typing and duck typing. That logically cannot be what makes them different.
My original point was that TFA doesn't really feature duck/structural typing; the example program just shows dynamic upcasting/downcasting, which has been a staple of OOP since forever, and has no relation to duck/structural typing.
The structural/duck typing thing is exactly about when the type check happens. That is the thing that makes them different. We aren't just throwing out random words. We would not be talking about them in the first place if not for that fact. To say it is not about the only reason we have for talking about them is... strange.
> My original point was that TFA doesn't really feature duck/structural typing
It does, indeed. It demonstrates both, in fact. Go uses nominal typing for everything but interfaces. Is that the source your confusion?
> the example program just shows dynamic upcasting/downcasting
I know we threw casting around loosely to not confuse the contextual parent, but if you look closely, Go doesn't even support casting at all. And this isn't type conversion either. Go uses the syntax `type(variable)` for that. This really has nothing to do with what was seen in the article; it was doing something quite different.
To be fair, I'm not sure you have demonstrated that Java has casting either. What you wrote appears to be much more like type conversion, except type conversion is checked statically, while yours was checked at runtime. Frankly, I have never heard of a term for what you have shown, so I don't know what to call it. I suppose casting, overloaded with new meaning, really is that term[1]? Go doesn't support casting in that sense either, though.
[1] Which, if that is the case, would make the rest of this thread quite hilarious with your attempt to be the language police for "duck type", yet being accepting of any arbitrary use of "cast".
Surely that's not the first time you've heard of run-time type casts? Well, in any case, they are colloquially called "type casts" and/or "type conversions".
To be more pedantic, Java calls it "casting conversion" [0] which, in this specific case, allows one of the "narrowing reference conversion" [1] to happen.
Go calls their version "type assertion" [2] but if you read what it actually does ("If T is an interface type, x.(T) asserts that the dynamic type of x implements the interface T") and compare with what Java does ("Such conversions require a test at run time to find out whether the actual reference value is a legitimate value of the new type"), I hope you would agree they are essentially the same.
We can also take a look at C# [3]: it has "cast expressions" which work like this: "A cast-expression of the form (T)E, where T is a type and E is a unary-expression, performs an explicit conversion (§13.2) of the value of E to type T". And in this example, the exact kind of conversion to be performed would be 13.2.3, "Explicit reference conversions" for which the following is stated: "The explicit reference conversions are those conversions between reference-types that require run-time checks to ensure they are correct".
So TFA could be written in any of those languages (or even in C++ with its dynamic_cast<T>) and look almost exactly the same because all TFA does it takes some objects, converts them up to Object type that subsumes all other types (in Golang's case, this type is called "any"), then tries to convert them down to some interface types which those objects may or may not implement, and blows up. But neither Java, C#, nor C++ have neither structural nor duck typing so TFA's claim that somehow this example shows that Go has duck or structural typing is simply baseless.
[0] https://docs.oracle.com/javase/specs/jls/se6/html/conversion...
[1] https://docs.oracle.com/javase/specs/jls/se6/html/conversion...
Indeed – and for good reason, I'm sure. Pedantically, a cast is where the programmer says in a language "just trust me on what type this is, don't bother to check my claim". A runtime type cast doesn't make any sense.
I mean, I'm fine with words evolving and whatnot. I'm not here actually trying to play language police myself. But, as it seems you want to squabble over "duck type", which is then quite humorous when you accept any random definition someone can dream up for "cast": Why not call what Java is doing duck typing?
> I hope you would agree they are essentially the same.
I do agree, just as I said before, that Go's type conversions are essentially the same, except checked at compile time rather than run time. There is no conversion with type assertions, however. It is merely a runtime type check. The type you have is the type you keep.
Now, what is important to remember is that Go's interface is a structural type. And what is a "structural type" checked at run time? That's right: A duck type!
> So TFA could be written in any of those languages
It's all just 1s and 0s at the end of the day. Of course you can write something that achieves the same result in just about any language. There is more nuance at play here, though. In the case of Go, it is not "here is another way to achieve the same thing". It literally employs duck typing, as duck typing is normally understood.
In practice, the use of dynamic is highly discouraged as well as anything that could lead to type confusion i.e. currently discussed Go's duck typing it calls structural (runtime vs static is impl. detail).
Instead, you are expected to simply do 'lessSpecific is IMoreSpecific ms`, works particularly well with pattern matching on switches for functional style.
I think this is an important detail, because it’s way safer than in C, where you can just re-interpret memory accidentally and the compiler won’t stop you. Go will panic the thread. Ok, but we already knew that.
The subtle bit is that it’s even a bit safer than most dynamic languages like Python. Because the programmer has to explicitly handle the “unexpected type” case. And because of how verbose Go’s error handling is, every caller up the stack has to consider the error, enforced by the type system. This is different than in Python, where every variable is type any at compiler time, so every line can throw a time error and the callers probably don’t handle it. Even when using “any”, Go’s type system enforces that both the caller and callee consider the “unexpected variable type case”. Overall, it’s still way more of a controlled failure compared to dynamic languages.
It’s great that Go has generics now, to do away with some any hacks. And in general you should avoid any in Go. But when you need it, I think Go actually has a really great reflection API (especially with how it composes with other features like error handling). It’s a bit looked over I think because the difference is subtle at first glance.
TypeScripts type system is commonly described as implementing "structural types" (not "duck" typed)
And there are a ton of pragmatically sound gotchas around this, such as the importance of whether an object is used as an argument or a parameter, or if it is declared using an object literal.
(focusing on objects here because for primitives I'd assume that TS has almost nominal typing)
So, is there an ELI5 for the difference between structural typing and duck typing?
I'd guess that duck typing is more similar to the callee perspective in TS (give me an object with the properties I need), while structural typing is more similar to the checks performed when initializing an object literal with a declared type?
But I'm not sure if I'd be able to differentiate both terms succinctly.
What is the relationship between duck typing and "type narrowing"? Or is that more of a "structural typing" thing?
Likely just semantics. You could argue that the sliver of difference is just treating something like a particular API shape hence duck typing over structurally matching to an interface. For practical purposes however it does not matter. The runtime vs compile time argument is purely an implementation detail that has no relation to structural matching aka duck typing (heh) otherwise.
In practice, I find this idea rather uncomfortable and C# has been avoiding it except for few very defined cases that are least likely to cause this confusion, like GetEnumerator and Enumerator.MoveNext+Current (it could be used as an enumerable in foreach loops but unless the type implements IEnumerable, you can't coerce it to the type).
For the cases where specialization is needed, it is usually written as
// could also be Handle<T> where T: IDataArg to avoid boxing for structs
// as struct generics in .net are zero-cost via monomorphization
void Handle(IDataArg arg) {
if (arg is IPooledArg pooled) {
var rented = arg.Rent();
// do something with the arg
rented.Return();
}
// slow path
}
No type confusion is possible as whatever is passed to the method has to declare it implements IDataArg. Same applies to 'arg' being 'IPooledArg' - it could have methods with names that would make it match the interface structurally but this is, quite literally, not a type-safe assumption. The authors may have explicitly decided not to expose it, or to provide explicit interface implementation that differs from pre-existing names.All this is very upfront and you won't be finding yourself having to rename methods or be surprised when your objects or structs are used in a way they are not supposed to because their triangle-shaped type happened to fit in a square-shaped interface hole.
I think I share your perception of ugliness in TS.
It feels like in TS, most if not all of the worries around complex types originate in compromises like this, or in other words, structural/duck typing.
Also feels a bit like functions are an elephant in the room. Function types with structural typing for parameters, arguments and return values are useful.
But then you have to use overloads and "extends" in generics for the first time in order to *describe* something, and if you're like me, you might cry a little from the inside. Especially when you simultaneously chastise yourself for wasting time on ignorable TS issues.
While similar, structural typing is seen statically (think compile time), duck typing is seen dynamically (think run time).
So I will go on to imagine duck-typing as the classic JS way where functions started with often pretty extensive runtime checks of their parameter values.
I guess this also means that duck-typing is inherently tied to checking primitive types and/or object structures, return values etc at runtime.
type A = { a: string; b: string }
type B = { a: string; b: string; c: string }
If you created a new object like this: { a: “foo”, b: “bar”, c: “baz” }
It would satisfy both type A and type B.That won’t work quite so easily in go without messing around a bit.
So far it hasn't bitten me in the behind, but I wouldn't advise anyone to use that logic for production code.
The difference between structural typing and duck typing is that one is evaluated at compile time, while the other at runtime. Typescript is erased before runtime, so it by itself does not exhibit duck typing features, although Javascript does.
The article is about how it does support duck typing in the usual sense. Why do you disagree?
Sure, same as any other language with duck typing. Consider Ruby, which is famous for its duck typing. `variable.not_a_method` raises NoMethodError. How do you think it knows to do that? That's right, it first performs a type check and, since that check fails, it doesn't try to call the method and instead raises the exception. Exactly the same as Go panicking under the same circumstances.
> and then to something with a specific interface.
That is a type assertion, not a cast (Go doesn't have support for casts anyway). It merely asks the runtime: "Does this value satisfy the given type?" There is no change in type.
Now, let's take this further: It is generally agreed upon that duck typing differs from structural typing only in that it is evaluated dynamically instead of statically. Go's interface is quite clearly a structural type when evaluated statically.
And what do you think the type assertion does in the example given? That's right: It performs a type assertion on a "structural type" at run time. Duck typing, by definition!
> I don;t consider C a duck typing language too, because you can write ((Bird) ptr)->swim(), but perhaps you do?*
No. First and foremost, where would the typing come into play? That will actually try to call the method, whether the pointed memory is suitable or not. There is no type checking to steer you around mistakenly calling something that shouldn't be called. Not at compile time, not at run time.
It is literally called duck typing, not duck valuing. "Type" has significance. Where do you find it here?
This can be thought of as duck typing.
Examples can be found here: https://blog.merovius.de/posts/2017-07-30-the-trouble-with-o...