[HN Gopher] Duck typing (horror) in Go
___________________________________________________________________
Duck typing (horror) in Go
Author : thunderbong
Score : 35 points
Date : 2024-08-21 17:49 UTC (5 hours ago)
(HTM) web link (www.ccampo.me)
(TXT) w3m dump (www.ccampo.me)
| loosescrews wrote:
| So Go supports both structural and duck typing? Why is that a
| problem?
| tgv wrote:
| It doesn't support duck typing in the usual sense, but
| unchecked type casting 'any' to an interface does indeed
| compile. To any type, as a matter of fact.
| randomdata wrote:
| _> It doesn 't support duck typing in the usual sense_
|
| The article is about how it does support duck typing in the
| usual sense. Why do you disagree?
| moritzwarhier wrote:
| I have a stupid question (web dev, but also have used Java and
| C#).
|
| 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?
| randomdata wrote:
| _> So, is there an ELI5 for the difference between structural
| typing and duck typing?_
|
| While similar, structural typing is seen statically (think
| compile time), duck typing is seen dynamically (think run
| time).
| neonsunset wrote:
| > So, is there an ELI5 for the difference between structural
| typing and duck typing?
|
| 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.
| ljm wrote:
| In TS you can have two types, say: 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.
| simonz05 wrote:
| This type of runtime type checking is quite common in Go. An
| example from bufio.Reader's WriteTo method: //
| 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() }
| yegle wrote:
| Interface smuggling:
| https://utcc.utoronto.ca/~cks/space/blog/programming/GoInter...
| glenjamin wrote:
| The Go standard library makes use of a number of "optional
| interfaces", which it uses to detect when a type implements a
| higher level action in a more efficient or specific way
|
| 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...
| mseepgood wrote:
| Sorry, but where's the "horror"? I only see the description of a
| great feature.
| dilap wrote:
| Confusing post; as I see it, it doesn't really have much to do
| with duck-typing.
|
| 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.
| randomdata wrote:
| _> 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._
|
| 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.
| Joker_vD wrote:
| How is that any different from doing 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?
| randomdata wrote:
| _> Do those languages now have duck typing_
|
| 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?
| dilap wrote:
| I think the closest equivalent in Java or C# would be to
| use reflection to pull out the Swim and Quack methods.
| mseepgood wrote:
| In C# you would simply use the 'dynamic' keyword.
| neonsunset wrote:
| And get yelled at by your colleagues ;D
|
| 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.
| Joker_vD wrote:
| Yes, the runtime type check does happen, see [0]. The
| output is pretty much the same as with the Go example,
| only the exact wording of the exception/panic is
| different.
|
| 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.
|
| [0] https://godbolt.org/z/vndns4619
| dilap wrote:
| Ah, ok, thanks, now I see the point the article was trying to
| make.
|
| Yes -- supporting structural typing + also runtime casts
| means you support duck typing.
| materielle wrote:
| I would point out that there is a special syntax / api when you
| deference "any" in Go. And you are explicitly prompted by the
| language to consider the case where you receive an unexpected
| type.
|
| 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.
| GrantMoyer wrote:
| I think the Wikipedia article is simply wrong that duck typing is
| exclusive to dynamic type systems.
|
| 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.
| iamcreasy wrote:
| > structural typing means that use of an object type checks if
| some explicitly specified set of operations on it are defined
|
| 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...
| seanmcdirmid wrote:
| Structural subtyping is used in most ML-ish languages,
| including F# and Haskell.
| GrantMoyer wrote:
| I was mistaken in the prior version of my comment when I said
| Crystal uses structural typing. As a somewhat trivial C++20
| example though: 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.
| karmakaze wrote:
| It's no surprise. The example is ignoring the error from casting.
| It should be: 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.
| 38 wrote:
| that an who the fuck uses interface literals? I do Go
| personally and professionally for years and never seen it.
| didip wrote:
| I was about to say, after so many years doing Go, I have
| never seen this and would love to read blog post about why
| interface literal is useful.
| mseepgood wrote:
| You can learn from the Go codebase to get a feel of why
| this is useful:
|
| https://github.com/golang/go/blob/master/src/cmd/go/interna
| l... https://github.com/golang/go/blob/master/src/io/fs/fs.
| go#L26... https://github.com/golang/go/blob/master/src/erro
| rs/wrap.go#... https://github.com/golang/go/blob/master/src
| /go/parser/resol...
| jayd16 wrote:
| But it mentions exactly this after the page break?
___________________________________________________________________
(page generated 2024-08-21 23:00 UTC)