Spreading object/record types to create new types?

I wonder if it’d be possible to allow defining new object/record types by spreading existing types now that ReScript becomes more of it’s own language? Something like this:

type foo = {
  field1: string,
  field2: int,
};
type bar = {
  ...foo,
  field3: bool,
}

I understand it’s probably not possible on OCaml type level, but could there be a PPX? Or is there already? Or could ReScript implement a language feature that practically works as a PPX. Or something else? Idk, this is above my current skill level :stuck_out_tongue:

It’s quite commont to do something like this in flow/TS, so this would make creating bindings to 3rd party libs easier, if nothing else.

One common use case is “React component prop drilling” where almost same props are passed to multiple components in the tree except that some components add a couple of new props.

Thoughts?

I understand it’s probably not possible on OCaml type level

Hello! Yeah that’s what the object system already is. The point of a record is precisely not being an object (aka nominal, fixed fields for perf and error message sanity). The object feature has various tradeoffs both in theory and implementation. It’s unclear whether we should default to that.

but could there be a PPX?

We might in a far timeline consider giving record types a subset of the abilities you probably expressed (this is an old topic), but never, ever in PPX form. We’re trying to have fewer ppxes. This one definitely isn’t a worthwhile case, if only because it’s almost impossible to robustly implement it with ppx considering the type definition might come from another file.

It’s quite common to do something like this in flow/TS, so this would make creating bindings to 3rd party libs easier, if nothing else.

Folks have been using the object system for precisely this actually.

One common use case is “React component prop drilling”

This info got lost in the previous chat channel, but React prop spread is an explicit non-goal. We very intentionally ask you to destructure what you need and assign explicitly. Prop spreading in the simplest case sounds ok, but then in real codebases folks try all sort of higher-order stuff with it. It’s been a nightmare to maintain the complex cases because you have no idea what happened where and an extra prop innocently passed somewhere in the middle now has performance ramifications (memoization etc) and other semantic ramifications. In this case, being explicit really saves on future debugging time.

Anecdote (testing water on various usages of this forum): I recall in the earlier days I had to help folks debug some usage of a popular reactjs form library. The main components heavily abused the above patterns: at one point there were 60+ props passed around because the features above made it so easy. One of the main renders destructured a few props out of 60 and, when compiled, resulted in a loop of 60^2 iterations. Funnily the user said they’ll optimize this later. The right optimization is to stop doing all that.

6 Likes

Prop spreading for prop drilling is in fact an anti pattern. However, having a known type and being able to spread it to create a new type doesn’t need necessarily to be tied to prop drilling. I think it is an useful feature that Ocaml has to some degree with modules. You can include a module into another module and have all its props and methods, this is some kind similar.
However, I have been using Flow without spread support and with it (for types) and I didn’t noticed any noticeable productivity boost

1 Like

OCaml has ‘object spread’ already:

type person = < name : string >
type employee = < person; id : int >

You can do this today in ReScript if you want, in OCaml syntax.

3 Likes

I think the only case where props spreading might be a non-smell is when the value being spread is the only thing that you pass to a component, like this:

<Foo {...p} />

Because then it gets compiled straight to:

React.createElement(Foo, p);

But when it’s <Foo {...p} x={x} />, Object.assign kicks in and all hell breaks loose :slight_smile:

Ocaml is the syntax I that I prefer so I’m glad it’s already possible

Ooh, I didn’t know this is possible in OCaml.

I mean, this is the only thing I’m even wanting/proposing here. I also agree prop spread in actual variables (or react props) is an antipattern.

But I encountered this problem when creating bindings for react-select version 3. In many of its components it defines the prop API like this: https://github.com/JedWatson/react-select/blob/4fb2fd608f475efac911fe132184fad913762134/packages/react-select/src/components/Option.js#L24
I think the rationale for that is to allow full flexibility for users to override any of the components and still gain access to all possible props, some of which the default implementation might not even use. It can be of course argued whether that is an anti pattern or not, but in practical life the person trying to use this library has following options:

  1. Copy-paste the same list of common props (> 10) as many times as there are components (> 10) to create exhaustive bindings. (Feels like a bit too much, and it breaks easily too because there is lots of duplication.)
  2. Only create bindings for features they personally use, which means bindings cannot be meaningfully shared as OSS
  3. Rewrite the whole lib (tempting, but some of the libs are quite big and complicated, react-select is one of them)

None of those options seem great, so It would be awesome to see the OCaml feature land to Reason/ReScript too. But of course it would need to work for objects too I think.

There are definitely some use cases where row polymorphism is quite efficient at modeling a problem.
Since OCaml 4.06 (and hence available in latest Rescript) you can do this in ocaml syntax:

type person_obj = < name : string > 
type person = person_obj Js.t
type employee = < id : int; person_obj > Js.t

Though, Rescript purposely doesn’t ship with the Ocaml object and class system, so a workaround should be found to express the same thing with rescript syntax:

type person = {
  "name": string,
}
type employee = {
  ...person,
  "id": int,
}
2 Likes

That’s precisely one of the reasons why include is discouraged. To some degree it’s a fancy compile-time copy paste of entire modules again and again. Horrible for code size.

To be fair this is more like including a module type inside another module’s signature. This doesn’t add any code size.

1 Like

Functors do this, but I don’t believe include does :thinking: certainly it hasn’t impacted the code size where I’m looking (I have a library module that does include Js.String2 and adds a few extra methods).

Might wanna keep an eye on your output then. Js.String2 is externals-only; one of the many reasons why we advocate externals over wrappers. So the output happens to be empty and you only pay the compilation cost, which is also very non-trivial because you’ve essentially added another file to the compilation each time you do include. There are optimizations to short-circuit things but you get the idea.

Btw, what explains the vast compilation performance of Go & OCaml vs Rust & C++ isn’t the engineering expertise (all of them are very talented) but actually the generic monomorphization strategy (aka the former 2 don’t). include and functors are our own heavyweight “generic” specialization features; thankfully we don’t have more of those concepts. To avoid ending up with a slow and heavy (in more than one way) compilation, use less of them.

1 Like

Hi, this can not be done elegantly in ppx, it needs some more internal changes, it’s a nice wish though.

As someone pointed out, it is already possible in structural types.

So for example, the types below is supported and recommended

type t0 = < x : int >  
type t1 = < t0 : t0, y : float > 
let u : t1 Js.t = [%obj { x = 3 ; y = 3.0} ]

We can still make use of structural types, it is such things below not recommended:

let u = object method x = 3 method y = 3.0 end

It’s not recommended due to the fact that we can not compile it efficiently

It’s probably worth mentioning that these kind of types are causing a lot of terrible error messages in case of a type mismatch, and I’d hate to see this feature exposed without proper facilities to fix type errors.

1 Like

Ah, that explains it. We only use inline sparingly, though, in our stdlib (I can see in one of the other modules it does add a bunch of vars re-exporting the API) and some Functor code.

I have assumed tree shaking bundlers can inline these re-exported API links so it won’t have any impact on prod bundle size, but I haven’t checked.

So what’s the pattern/method of achieving something like this when trying to model things that are built upon base types?

type person = {
 name: string
}
type employee = {
 ...person,
 company: string
}

How do you model this idiomatically with rescript/reason?

There is no inheritance or “merging of types” in ReScript.
One possible approach would be nesting of records:

type person = {
  name: string
}

type employee = {
person: person,
company: string
}

If you need to store values of both types together (e.g. in an array), you could use variants as well:

type person = {
  name: string
}
type employeeDetails = {
  company: string,
  salary: float
}
type employerDetails = {
  company: string,
  employees: int
}

type role = | Person(person) | Employee(person, employeeDetails) | Employer(person, employerDetails)

This way it’s also easily possible to pattern match over those types.

This would actually be possible with ocaml objects and hence with rescript JS objects but it lacks the syntax for it. Hopefully it will be added because I think there are use cases for this. Maybe create an issue in the syntax repo?

See Spreading object/record types to create new types?

You can do this at this time

type o = {"hi": int}
type oo = {"hi": int, "lo": int}

let u: o = {"hi": 3}
let uu: oo = {"hi": 3, "lo": 2}

In the future, we plan to remove the need for Js.t, so you can use it as first class.

There seems to be a bug in the rescript syntax, I filed here https://github.com/rescript-lang/syntax/issues/154

In ocaml syntax, you can do it like this

type o = < hi : int >
type oo = < o ; lo : int >

let u : o Js.t = [%obj{ hi = 3 }]

let uu : oo Js.t = [%obj { hi = 3; lo = 1}]
1 Like