Canonical way to represent idea of sum type of records that all extend a "base" record

Viewed 53

I'm new to PureScript. I was searching for sealed classes in Purescript to get an idea of how one would implement this, but I don't think I have the necessary PS jargon yet.

What is the canonical way in PureScript to have a bunch of records that extend a "base" record, but then have one sum type representing a "sealed" collection of those.

Something like in Kotlin,

sealed class Glazing(val name: String, val area: Int) {
  class Window(val name: String, val area: Int, val frameMaterial: String): BaseGlazing(name, area)
  class Door(val name: String, val area: Int, val isPreHung: Boolean): BaseGlazing(name, area)
}

in TypeScript, you'd probably do something like

interface BaseGlazing { ... }
interface Door extends BaseGlazing { ... }
interface Window extends BaseGlazing { ... }
type Glazing = Door | Window

and then you'd either take `A extends BaseGlazing` or `Glazing` (and use type guards) to do either of those two above functions.

Essentially I want a base class (that is technically abstract), things that extend it, and then a sum type/discriminated union of the extensions so that way I can both write, say, changeName:: Glazing -> Glazing (premised on the base class having a name prop) but also do something like calculateTotalLightPenetration :: Array Glazing -> Number (premised on the discriminated union being one of Door or Window since light penetration will be a different formula for doors vs windows)

1 Answers

The idea of "inheritance" (aka "is-a" relationship) is technically possible to model in PureScript, but it's hard and awkward. And there is a good reason for it: inheritance is almost never (and I am tempted to say "never, period") the most convenient, efficient, or reliable way of modeling the domain. Even OOP apologists tend to recommend aggregation over inheritance these days.

One useful thing to observe is that you don't actually need inheritance. What you need is to solve some specific problem in your domain, and inheritance is just a solution that you naturally reach for, which is probably informed by your past experience.

And this leads us to an insight: the particular way to model whatever it is you're modeling would depend on what the actual problem is. Chances are, PureScript has a different mechanism for modeling that.


But if I base my thinking on the specifics you gave in your question (i.e. the changeName and calculateTotalLightPenetration functions), I would model it via aggregation: the "glazing" would be the surrounding type, and it would have, as one of its parts, the specific kind of glazing. This would look something like this:

type Glazing = { name :: String, area :: Int, kind :: GlazingKind }

data GlazingKind = Window { frameMaterial :: String } | Door { isPreHung :: Boolean }

changeName :: Glazing -> Glazing
changeName g = g { name = "new name" }

calculateTotalLightPenetration :: Array Glazing -> Number
calculateTotalLightPenetration gs = sum $ individualPenetration <$> gs
  where
    individualPenetration g = case g.kind of
      Door _ -> 0.3
      Window _ -> 0.5
Related