[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)