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.
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.
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/
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/manifoldYou 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.
Like having `<T: {width: f64, depth: f64}>`?
I have such a hard time understanding the multiple arrows notation of ML family languages.
Is this valid rust (it’d be new to me)?!
If not, I’m guessing, from memory, the only way at this in rust is to through traits?
type alias Furniture = { name : String, length : Float, width : Float, height : Float}
type alias Room = { name : String, length : Float, width : Float}
floorArea : { a | length : Float, width : Float} -> Float
floorArea {length, width} = length * width
This is just an example for brevity. In practice, I'd probably recommend the dimensions be stored as `Length.Length` from the package elm-units. I'd modify the extensible record argument of `floorArea` to expect the same, and I'd have `floorArea` return an `Area.Area`.You can also use nominal typing to prohibit this via opacity. You can then wrap up construction in a function that fails for invalid states. Here we export the type Furniture but not its only constructor Furniture1.
module Furniture exposing (Furniture, width, length, height, name)
type Furniture = Furniture1 { name : String, length : Float, width : Float, height : Float}
name : Furniture -> String
name (Furniture1 {name}) = name -- etc for width, length, height
type Dimension = Width | Length | Height
type Error = NameCannotBeBlank | DimensionMustBePositive Dimension
safeConstruct : {width, height, name, length} -> Result Error Furniture
safeConstruct params =
if params.name == "" then
Err NameCannotBeBlank
else if (params.width <= 0) then
Err (DimensionsMustBePositive Width)
-- repeat for length and height
else
Ok (Furniture1 input)
then in a different module (i.e. a different file) (assume I also wrote safe constructors and accessors) module Room exposing (Room, width, length, name)
type Room = Room1 { name : String, length : Float, width : Float}
Finally, your floor area function would not be able to inspect values of these types because we did not expose the Floor1 and Room1 constructors (in practice those would be named Floor and Room but that's confusing in this example). It would need accessors for the properties. floorArea : { toWidth : a -> Float, toLength : a -> Float } -> a -> Float
floorArea {toWidth, toLength} = (toWidth a) * (toLength a)
floorAreaRoom : Room -> Float
floorAreaRoom = floorArea { toWidth = Room.width, length = Room.length }
floorAreaFurniture : Furniture -> Float
floorAreaFurniture = floorArea { toWidth = Furniture.width, length = Furniture.length }
Also in practice I'd maybe have `safeConstruct` have a shorter name and return a non-empty list of errors rather than just the first error, but that would cloud this example.
skybrian•6mo ago
tines•6mo ago
wavemode•6mo ago
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•6mo ago
jerf•6mo ago
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•6mo ago
tines•6mo ago
sparkie•6mo ago
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:
In OCaml, you'd just inline the row types - there are no `Foo` or `Bar`: You can typedef row types though 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•6mo ago
aatd86•6mo ago