[HN Gopher] Row Polymorphic Programming
       ___________________________________________________________________
        
       Row Polymorphic Programming
        
       Author : todsacerdoti
       Score  : 44 points
       Date   : 2025-07-14 18:50 UTC (4 days ago)
        
 (HTM) web link (www.stranger.systems)
 (TXT) w3m dump (www.stranger.systems)
        
       | skybrian wrote:
       | What could you do with row polymorphism that you couldn't do with
       | generic functions that take slices in Go?
        
         | tines wrote:
         | I think Go interfaces are row-polymorphic aren't they? I'm also
         | wondering what the difference between row-polymorphism and ad-
         | hoc polymorphism (a la C++ templates) is.
        
           | wavemode wrote:
           | Yes, Go interfaces are row-polymorphic, in the sense that
           | they allow you to write functions that operate on any struct
           | which has a field with a certain name and type.
           | 
           | This utilizes the fact that structs implement interfaces
           | implicitly in Go, rather than explicitly.
           | 
           | > I'm also wondering what the difference between row-
           | polymorphism and ad-hoc polymorphism (a la C++ templates) is
           | 
           | C++ templates can also be row-polymorphic. They're a lot more
           | flexible than just row polymorphism, though, because they
           | essentially allow you to be polymorphic over any type for
           | which a given expression is valid syntax.
           | 
           | Concepts were an attempt to allow developers to rein in some
           | of that flexibility, since it actually became a pain.
        
             | skybrian wrote:
             | Is this new with generics? I thought Go interfaces were
             | just method sets?
        
             | jerf wrote:
             | "Go interfaces are row-polymorphic, in the sense that they
             | allow you to write functions that operate on any struct
             | which has a field with a certain name and type."
             | 
             | It doesn't have it yet. Go is headed strongly in that
             | direction which is why I add the "yet", but see
             | https://github.com/golang/go/issues/70128 , especially the
             | "future directions" note about issue #48522 which is not
             | implemented. Prepatory work has been done but the change is
             | not in yet.
             | 
             | You can embed something like compile-time row types into Go
             | if you implement it in terms of accessor methods that can
             | be included in an interface, rather than direct struct
             | field access, which has its pros and cons.
             | 
             | But if you're reading this in 2026 or beyond, check in with
             | Go, this may not be true anymore.
        
         | aatd86 wrote:
         | When you want to abstract on structs "having" a given field (by
         | name) of a given type, I think. Although that may come in Go at
         | some point.
        
           | tines wrote:
           | I'm not a Go expert but that's exactly what interfaces are,
           | aren't they? In Go a struct automatically implements an
           | interface if it contains all the required fields (duck-
           | typing).
        
             | sparkie wrote:
             | They're both a form of _structural typing_ - the static
             | equivalent of duck typing.
             | 
             | A difference with row types is that they don't have to be
             | declared up front like an interface - they're basically
             | anonymous types. You declare the row types in the type
             | signature.
             | 
             | For example, a function taking some structural subtype of
             | `Foo` and returning some structural subtype of `Bar` would
             | be written in Go as:                   type Foo interface {
             | baz Baz         }                  type Bar interface {
             | qux Qux         }                  func (x Foo) f() Bar {
             | ...         }
             | 
             | In OCaml, you'd just inline the row types - there are no
             | `Foo` or `Bar`:                   val f : < baz : Baz.t; ..
             | > -> < qux : Qux.t; .. >          let f x = ...
             | 
             | You can typedef row types though                   type foo
             | = < baz : Baz.t; .. >         type bar = < qux : Qux.t; ..
             | >         val f : foo -> bar
             | 
             | Rows can be declared to have an exact set of members. For
             | example, the types `< bar : Baz.t >` and `< bar : Baz.t;
             | ..>` are two different types. The latter can have members
             | besides `bar`, but the former can only have the member
             | `bar`. A type containing the fields `bar` and `qux` would
             | be a subtype of `< bar : Bar.t; .. >`, but it would not be
             | a subtype of `< bar : Bar.t >`.
             | 
             | OCaml has another form of structural typing in its module
             | system, closer to interfaces where a `module type` is
             | declared up-front, and can be specified as the argument for
             | a functor (parameterized module) - but this is incompatible
             | with the row types in the object system.
        
               | logicchains wrote:
               | Go also supports inline/unnamed interfaces.
        
             | aatd86 wrote:
             | It's not fields but methods.
        
       | moron4hire wrote:
       | Also called Nominal Typing. I generally consider it a mistake.
       | Interfaces aren't just a bag of fields and methods. They encode
       | semantics, too. Otherwise, you could dismiss employees with a gun
       | or by loading them into a kiln.
        
         | tines wrote:
         | Nominal typing is the opposite, I think you're thinking of
         | structural typing, no?
        
           | moron4hire wrote:
           | Yep, sorry
        
         | taeric wrote:
         | I am not entirely sure I disagree. I think context is often a
         | big factor in semantics, though? The problem with interfaces is
         | often that people think they do all of the work by themselves.
         | But, that is only true within the context of how you use some
         | data. And people tend to accidentally constrain themselves too
         | heavily.
         | 
         | You can make the same argument for numbers, for an easy
         | exploration. Just look at all of the tools that you can have at
         | your disposal by thinking of things as numbers.
        
         | michael1999 wrote:
         | In the original relational papers, column names were basically
         | types. In a OO world, that doesn't make sense, but in a
         | relational world, it does.
        
         | pxeger1 wrote:
         | I think you mean structural typing; nominal typing is the
         | opposite, where field names are lexically scoped.
         | 
         | Anyway, row polymorphism can technically be used with nominal
         | typing, it's just that it usually makes sense to use structural
         | typing instead.
         | 
         | The key benefit of row polymorphism is a bit of an
         | implementation detail - it lets you get something resembling (a
         | limited form of) subtyping in your language, without needing as
         | complicated a type inference algorithm as fully-general
         | subtyping requires.
         | 
         | Row polymorphism can be (IMO should usually be) made opt-in, so
         | you can avoid problems like the scenario you describe.
        
       | daxfohl wrote:
       | The only thing is I wish there was a way to safely lift a row-
       | polymorphic record to a named record type if the compiler can
       | determine it has all the required fields. Haven't seen any
       | languages that offer this yet though.
        
         | continuational wrote:
         | Firefly can construct any named record from any other record
         | (named or anonymous):                   let point = (x = 5, y =
         | 7)         let point3d = point.Point3d(z = 0)
         | 
         | We included this feature specifically to make it easy to use
         | named and unnamed records together.
         | 
         | More here: https://www.firefly-lang.org/
        
         | owlstuffing wrote:
         | The manifold project[1] for Java provides that feature as
         | "Structural Interfaces" which supports polymorphic variants.
         | 
         | The project also supports tuples that behave similarly.
         | var person = (name: "Joe", age: 35);         . . .
         | Person p = person; // tuple's name, age properties satisfy
         | Person record
         | 
         | 1. https://github.com/manifold-systems/manifold
        
       | owlstuffing wrote:
       | I hadn't come across the "Row" polymorphism term before, but it
       | sounds more like structural typing -- for example TypeScript, and
       | to a lesser degree Go, have structural interfaces that provide
       | "row polymorphic" programming.
       | 
       | You could go further with _variant_ structural typing, basically
       | this is a broader form of type checking based on _call_
       | compatibility, which answers the question -- is B#foo() callable
       | as an A#foo()?
       | 
       | For instance, your `area` example requires `double` result types,
       | otherwise a row type having `width` and `length` defined as
       | `integer` columns doesn't satisfy `area`. But, result types are
       | naturally _covariant_ -- `integer` is a subset of `double` --
       | which permits us to accept `integer` in the implementation of
       | `area`.
       | 
       | Similarly, parameter types are naturally _contravariant_ --
       | `Shape` is contravariant to `Triangle`, thus `B#foo(Shape)` is
       | call-compatible as a `A#foo(Triangle)`, therefore I can pass a
       | Triangle to `B#foo(Shape)`.
       | 
       | The manifold project[1] for Java is one example where the type
       | system is enhanced with this behavior using structural
       | interfaces.
       | 
       | Note, this goes further with parametric types where function
       | result types and parameter types define variance and other
       | constraints.
       | 
       | 1. https://github.com/manifold-systems/manifold
        
       | aerzen wrote:
       | Someone help me, a rust programmer , understand this: is this
       | like having a function be generic over structs with some fields?
       | 
       | Like having `<T: {width: f64, depth: f64}>`?
       | 
       | I have such a hard time understanding the multiple arrows
       | notation of ML family languages.
        
         | malcolmgreaves wrote:
         | Yes! The idea is that we just care that it's something with the
         | named & typed fields. It can have more fields, but we're really
         | setting a lower bound on what we need to exist for our function
         | to work.
        
       | malcolmgreaves wrote:
       | > In my personal experience I have found that when an experienced
       | programmer who really seems like they should know better is
       | making some weird sounding arguments about how type systems
       | decrease their productivity and don't prevent enough bugs to be
       | worth it, they seem to usually be complaining about the lack of
       | row polymorphism (or the closely related structural subtyping) in
       | popular statically typed languages, just without having the
       | technical vocabulary for it.
        
       | constantcrying wrote:
       | Interestingly Matlab has a specific data type for this, the cell
       | array, as far as I am aware the only language to provide a
       | specific data type for storing 2D arbitrarily typed data. Exactly
       | the use case described here.
        
       ___________________________________________________________________
       (page generated 2025-07-18 23:01 UTC)