1. 31
  1.  

  2. 4

    So…. is this different from structural typing / static duck typing?

    1. 12

      Good question. I guess it adds compile-time quack-checking?

      1. 9

        structural typing

        You could have structurally typed records without row polymorphism. You might say that { x: int, y: float } is the same type as { y: float, x: int }, but it’s not the same type as { x: int, y: float, z: string }.

        That’s how SML works. You can have two different record types with some fields in common:

        let a = { x = 1, y = 2.0 };
        let b = { x = 3, y = 4.0, z = "hello" };
        

        and then you can use the same field selector on both:

        let ax = #x a;
        let bx = #x b;
        

        But you can’t take #x and package it up into a function, because the type system has no concept of “any record type with an x field”:

        fun getX r = #x r;  // Error: unresolved flex record (can't tell what fields there are besides #x)
        

        static duck typing

        Are you thinking of Go interfaces, or C++ templates? (Or something else? :D) I think either one could plausibly be called compile-time duck typing.

        In Go you can use interfaces to have a function accept any struct that meets some requirements. But I don’t think you could express a function like this:

        // addColor has type { ...fields } -> { color: string, ...fields }
        let addColor (c : string) r = { color = c, ...r };
        let bluePoint2d = addColor "blue" { x = 1, y = 2 };
        let redPoint3d = addColor "red" { x = 3, y = 4, z = 5 };
        

        addColor can add a “color” field to any record type, without knowing which concrete type it was given. But the caller knows which record type it’s using, so it doesn’t need to cast the result.

        In C++ you can write getX as a templated function:

        template<class T, class S>
        S getX(T r) { return r.x; }
        

        But now the compiler doesn’t typecheck the definition of getX: it just waits for you to use it, and typechecks each use. That’s like duck typing (just pass it in, and see if it works), but at compile time. Just like with run-time duck typing, the problem is you can’t look at the signature of getX and understand what its requirements are.

        1. 1

          Are you thinking of Go interfaces, or C++ templates?

          Mostly TypeScript actually :) I’m not sure where the line is because TS calls what they do “structural typing” but I’m having a hard time seeing where the differences lie.

          1. 1

            Type inference for SML code using field selectors is hilariously bad, precisely because the natural and obvious principal types (having row variables in them) are inexpressible in SML.

            Out of curiosity, how would you expect your addColor function to work on records that already contain a color field? The purist’s answer would be to use something like Ur/Web’s disjointness witnesses, but, gawd, the syntax is super awful.

            1. 2

              Here’s an example in purescript that will only add color to records that don’t already have a color key:

              module Rows where
              
              import Prelude
              import Effect (Effect)
              import Effect.Class.Console as Console
              import Prim.Row (class Lacks)
              import Record as Record
              
              addColor ::
                forall r.
                Lacks "color" r =>
                String ->
                { | r } ->
                { color :: String | r }
              addColor color record = Record.union { color } record
              
              main :: Effect Unit
              main = do
                Console.logShow $ addColor "red" { foo: 1 }
               {- this won't compile
                  Console.logShow $ addColor "red" { color: "blue" }
               -}
              
              1. 1

                Fascinating. How is this Lacks type class implemented? Compiler magic?

                1. 2

                  It’s part of the Prim.Row builtin.

          2. 4

            It’s a kind of structural typing, so, I suppose it’s a kind of static duck typing.

            In general, with all of these, the more flexible the system the harder it is to work with and the less capable the inference is. Row typing is one way to weaken general structural subtyping by limiting the subtyping relationship to that which can be generated by the record/row type. So Row types are a bit easier to infer.

            1. 3

              I wrote a post a long time ago about differences between row polymorphism and (structural) subtyping: https://brianmckenna.org/blog/row_polymorphism_isnt_subtyping

              1. 1

                Oh I just realised my post was mentioned at the bottom of the posted one. Awesome!

            2. 2

              Is it really the record fields that are called “rows”, as this article states? I’d have thought the (sub)-record is the row, while the individual fields are more like “columns”.

              1. 3

                I’m wondering that too. I thought a row variable “stands for a whole row of types”.

                1. 1

                  Yeah that’s right, a row variable can be instantiated to 0 or many fields.

              2. 1

                I wrote a piece a couple years back on how you can use row polymorphism for “practical” cases (in my case it was tracking what kind of effects an HTTP endpoint could do, to avoid side effects on GETs)