Hacker News new | past | comments | ask | show | jobs | submit login

Row polymorphism is excellent. I was intro'd to it in Purescript, but I would like to say that Typescript gives you some things that rhyme with it through its combinations of records at the type level.

Highly recommend people mess around with Purescript, you can feel how much pressure is relieved thanks to the row polymorphism tooling almost instantly. Truly, all we wanted as an industry is an easy way to bundle together various tags into our types, and row polymorphism gets you there.

I think row polymorphism is a fairly straightforward thing compared to dependent types in general, but can let you crush a whole class of errors and pay almost nothing in terms of expression costs.




Somtime ago, there was a debate on the ability of a static type system to model an 'open-world situation' where fields are added to records as model changes. (based on a post[1] which responded to a Rich Hickey talk).

The crucial point was that structural typing on which row-polymorphism is based can model such open-world situations.

Also, having such a system can free you from having overly nested types.

It would be great if Purescript or row-polymorphism became more popular.

[1] https://news.ycombinator.com/item?id=22090700


This is also why I like interfaces in Golang (as a Haskell developer). You simply define a slice of the world you want to see and anything that fits the box can be passed.

It's just unfortunate golang interfaces dont support fields. Only methods. Typescript fares better with its interface type


> It's just unfortunate golang interfaces dont support fields. Only methods.

Why is that unfortunate? Usually when defining interfaces you care about the API surface without wanting to care about the internals of what will eventually implement that API. If you suddenly also spec fields with the interface, wouldn't be too easy to couple the internals to the API?

I can't say I've programmed in Go too much, so maybe I'm missing something very obvious.


The API surface of some struct is determined by visibility, not by whether a member of the struct is a method or a field.

I can't remember the specifics for why fields cannot be used within a Go interface but I do remember missing it a few times while writing Go code.


I think the reasoning is that interfaces are implemented by a dynamic lookup. Part of Go's philosophy is that things that could be expensive (function calls) should be visually distinct from cheap things.

Struct field access is cheap, hopping through a dynamic dispatch table is less cheap.


Took me a second to grok why the field access would require dynamic dispatch and it's because you have to deal with differing layout between structs.


> Highly recommend people mess around with Purescript, you can feel how much pressure is relieved thanks to the row polymorphism tooling almost instantly.

> I think row polymorphism is a fairly straightforward thing compared to dependent types in general, but can let you crush a whole class of errors [...]

Would you care to provide a few examples? I don't have experience with row polymorphism so I'm genuinely curious.


  greet :: forall r. { name :: String | r } -> String
  greet person = "Hello, " <> person.name <> "!"

  greetWithAge :: forall r. { name :: String, age :: Int | r } -> String
  greetWithAge person = 
    "Hello, " <> person.name <> "! You are " <> show person.age <> " years old."

  main :: Effect Unit
  main = do
    let person = { name: "Alice", age: 30, occupation: "Engineer" }

    -- greet can accept the person record even though it has more fields
    log (greet person)           -- Output: "Hello, Alice!"
  
    -- greetWithAge can also accept the person record
    log (greetWithAge person)


How does it differ from structural typing in TypeScript though?


Structural typing relies on interface compatibility. Row polymorphism is a type-level feature in PureScript where record types are constructed with an explicit "row" of fields.

In Practice, row polymorphism is more granular, allowing you to explicitly allow certain fields while tracking all other fields via a ("rest") type variable.

Example: PureScript allows you to remove specific fields from a record type. This feature, is called record subtraction, and it allows more flexibility when transforming or narrowing down records.

You can also apply exact field constraints; meaning, you can constrain records to have exactly the fields you specify.

Lastly, PureScript allows you to abstract over rows using higher-kinded types. You can create polymorphic functions that accept any record with a flexible set of fields and can transform or manipulate those fields in various ways. This level of abstraction is not possible in TypeScript.

These are just a few examples. In the most general sense, you can think of row polymorphism as a really robust tool that gives you a ton of flexibility regarding strictness and validation.


> PureScript allows you to remove specific fields from a record type. This feature, is called record subtraction, and it allows more flexibility when transforming or narrowing down records.

TypeScript does allow you to remove specific fields, if I understand you right [0]:

    function removeField<T, K extends keyof T>(obj: T, field: K): Omit<T, K> {
        const { [field]: _, ...rest } = obj;
        return rest;
    }

    type Person = { name: string; age: number };
    declare const p: Person;
    const result = removeField(p, 'age'); // result is of type: Omit<Person, "age">

> PureScript allows you to abstract over rows using higher-kinded types. You can create polymorphic functions that accept any record with a flexible set of fields and can transform or manipulate those fields in various ways. This level of abstraction is not possible in TypeScript.

Again, if I understand you correctly, then TypeScript is able to do fancy manipulations of arbitrary records [1]:

    type StringToNumber<T> = {
        [K in keyof T]: T[K] extends string ? number : T[K]
    }

    function stringToLength<T extends Record<string, unknown>>(obj: T): StringToNumber<T> {
        const result: Record<string, unknown> = {};
        for (const key in obj) {
            result[key] = typeof obj[key] === 'string' ? obj[key].length : obj[key];
        }
        return result as StringToNumber<T>;
    }

    const data = {
        name: "Alice",
        age: 30,
        city: "New York"
    };

    const lengths = stringToLength(data);

    lengths.name // number
    lengths.age // number
    lengths.city // number

[0] https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABA...

[1] https://www.typescriptlang.org/play/?#code/C4TwDgpgBAysBOBLA...

edit: provided links to TS playground


The tools typescript provides are a little pointless if it allows you to do stuff like this (imo):

const r1: { a: number; b: number } = { a: 10, b: 20 };

const r2: { a: number } = r1;

const r3: { a: number; b: string } = { b: "hello", ...r2 };

console.log(r3.b) // typescript thinks it's a string, but actually it's a number


Yeah, it's definitely not ideal, but even with its many flaws I prefer TS over plain JS.

The problem in question can be "fixed" like this

    const r1: { a: number; b: number } = { a: 10, b: 20 };

    const r2 = r1 satisfies { a: number };

    const r3: { a: number; b: string } = { b: "hello", ...r2 };
Now, TS would warn us that "'b' is specified more than once, so this usage will be overwritten". And if we remove b property -- "Type 'number' is not assignable to type 'string'"

Another "fix" would be to avoid using spread operator and specify every property manually .

Both of these solutions are far from ideal, I agree.

---

I don't advocate TS in this thread though; I genuinely want to understand what makes row polymorphism different, and after reading several articles and harassing Claude Sonnet about it, I still didn't grasp what row polymorphism allows over what TS has.


As far as I understand it, row polymorphism wouldn’t allow the given example. Or to put it another way, the spread operator is impossible to type soundly in the presence of structural subtyping because the type system doesn’t capture the “openness” of the record’s type, the potential presence of additional fields. Whereas with row polymorphism, to some degree or another, you can.


Thank you, but I was looking for real world examples solved by row types as OP implied there were plenty of.


Can `greetWithAge` be implemented using `greet`?


https://rtpg.co/2016/07/20/supercharged-types.html I wrote this example a long time ago, I think nowadays I could come up with some more interesting examples though.

As to the differences with TS... I think they're playing in similar spaces but the monadic do syntax with Purescript lets you use row polymorphism for effect tracking without having to play weird API tricks. In TS that's going to be more difficult.

(short version: in purescript I could write an API client that tracks its state in the types so that you can make sure you authorize before calling some mechanism. In TS you would need to design that API around that concept and do things like client = client.authorize(). Purescript you could just do "authorize" in a monadic context and have the "context" update accordingly)


PureScript is such a cool language. I wish it would get more traction, but it feels like it's competing in the same space as TypeScript, and TypeScript seems to solve the category of problems it addresses well enough for most people.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: