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