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.
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.
When is it "safe" though? What if the row-polymorphic record has more fields than the named record? Should those just be discarded? If it has exactly the same fields, then in C++ at least you can use std::get<NamedRecordType>(row_polymorphic_record) if you are using static polymorphism, and dynamic_cast<NamedRecordType*>(row_polymorphic_record) in case of dynamic polymorphism. Note that the compiler cannot determine anything at compile time when you have polymorphic objects, so it's just going to emit code that at runtime will throw an exception.
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.
> 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.
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.
This is roughly how records and extensible record arguments work in Elm. A `type alias` is a structural type, and a function argument specified as an extensible record `{ a | … }` can be read as "any record a having at least the fields …"
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)
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.
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.
10 comments
[ 2.6 ms ] story [ 31.3 ms ] threadYou 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
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?
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.
then in a different module (i.e. a different file) (assume I also wrote safe constructors and accessors) 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. 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.