Help with modelling types

Hi, I’ve been banging my head on this problem for a while, I feel like I’m trying to solve a puzzle here. It’s for my hobby project so it’s very low importance and if you value your time skip this question.

What I’m trying to do is to write a library similar to THREE.js in rescript (not bindings!)
And I’m trying to do something like inheritance but in a functional rescript way.

In THREE.js there’s an Object3D base class and all other things you can render extend Object3D

class Object3D {
  // a bunch of properties and methods like
  matrix: Matrix4,
  updateWorldMatrix() {}
  children: Object3D[]
}

class Mesh extends Object3D {
  // and some mesh specific thing
  geometry: number[] // simplified of course
}

and then Mesh can access it’s children, matrix, etc. And methods from Object3D work on all the things like Mesh.

So how can I represent something similar in rescript?

I want some base properties and methods that work on all things in the library (like Mesh), I want objects like Mesh to have access to the children (which can be anything that extends Object3D), I want to have an array with different kinds of things that extend Object3D.

Something that I’ve tried

type object3d = { matrix: Matrix4.t, children: NO_IDEA }

type mesh = { geometry: array<float>, object: object3d }

this kind of works, composition is quite nice but then I don’t really know which type to use for children.

Then I’ve tried something like this but then it’s just cyclic dependencies and kind of messy

type object3d = {matrix: Matrix4.t, children: array<node>}

let updateMatrix = (o: object3d) => o

type mesh = {
  ...object3d,
  geometry: array<float>,
}

type camera = {
  ...object3d,
  fov: int,
}

type node =
  | Mesh(mesh)
  | Camera(camera)

I’m a bit lost, help :slight_smile:

3 Likes

Calling @yawaramin with the bat signal:
image

3 Likes

I wonder if the dreaded first-class modules are a good fit here :upside_down_face:

Because generally, if you want something polymorphic and structural, modules (and functors) are usually the ReScript’s answer to classes. But since you probably want to populate children during runtime, compile-time modules won’t cut it.

2 Likes

https://rescript-lang.org/try?version=v11.0.0-beta.4&code=LYewJgrgNgpgBAWQIYBcBOBLAHgFjgXjgG8AoOOFATwAd4UC4MA7FEgXxJKtrhi3sIIYAZwAWACiIBzGCGAx0lAFxwkaNEkoAeZigB8bAJRwAPnADCSeRskAzEADcVuo527w0MAMZwQAIwArbxQAZjAGImBUTCwVZHRsHAA6FAAaOC9RDCgwTyYVNQ1tfyCvULA9dL4UFWqOElh6Tx8MFBg0AH0ohKwGcRAw9Nsu6OxjfD1iMjhh7pj+sKS5semAIRgoFCSAQXVNJPs0AFEkTIWwJMzs3JgmdJ8JxjbO5axxLyGRnsNDdk5GuDNJ7tLoiUR9AZgT4yOQKNCUcaTUjkYQAd1amV8i2qU3IZiEYkkMPkiiMBEmw2JcMo4ipil+eLgHXJcHEDLgHHI602Oz2lAOIGOpwkkMuWRyeXuLNaIPkhI+Mw6dPhPz+QA

module Matrix4 = {
  type t = int
}

type ext = Mesh({geometry: array<int>}) | Camera({fov: int})

type rec object3d = {matrix: Matrix4.t, children: array<object3d>, ext: ext}

let rec iter_matrix = (o3d, f_matrix) => {
  f_matrix(o3d.matrix)
  Belt.Array.forEach(o3d.children, c => iter_matrix(c, f_matrix))
}

let rec iter_mesh = (o3d, f_geometry) => {
  switch o3d.ext {
  | Mesh({geometry}) => f_geometry(geometry)
  | _ => ()
  }
  Belt.Array.forEach(o3d.children, c => iter_mesh(c, f_geometry))
}
6 Likes

This is a great question. You can do what you need to do without the class/object system (unless you need open recursion that is…but I’m pretty sure you don’t from your example).

Here is a quick and dirty example that may help you get started. It is using first class modules, gadt/existential types to make “objects”. I should note that depending on your needs, you may not need to pull out all the “fancier” type stuff…but here is one way if you’re curious.

(playground)

module M4 = {
  type t = float
  let default = () => 0.
}

module Object3d = {
  module type S = {
    type any
    type t

    // These are your "members"
    let matrix: t => M4.t
    let children: t => array<any>

    // Whatever methods you need
    let foo: t => unit
    let bar: t => unit

    let default: unit => t
  }

  // This packs the object methods with an instance
  type rec t = Object3d(module(S with type t = 'a and type any = t), 'a): t

  // Helper for creating these objects
  let v = (m, this) => Object3d(m, this)
}

/* You could even pull out some of the shared implementation into a functor. */

module A = {
  module T = {
    // See how any is the object type we defined above.
    type any = Object3d.t
    type t = {matrix: M4.t, children: array<any>}

    let matrix = t => t.matrix
    let children = t => t.children
    let default = () => {matrix: M4.default(), children: []}

    let foo = _t => Js.log("You called foo from A module")
    let bar = _t => Js.log("You called bar from A module")

    // Extra special A only functionality.
    let baz = _t => Js.log("You called baz from A module")
  }

  include T

  // Helper for packing A to the object type
  let to_object3d = t => Object3d.v(module(T), t)
}

module B = {
  module T = {
    type any = Object3d.t
    type t = {matrix: M4.t, children: array<any>}

    let matrix = t => t.matrix
    let children = t => t.children
    let default = () => {matrix: M4.default(), children: []}

    let foo = _t => Js.log("You called foo from B module")
    let bar = _t => Js.log("You called bar from B module")

    // Extra special A only functionality.
    let quux = _t => Js.log("You called quux from teh B module")
  }

  include T

  // Helper for packing A to the object type
  let to_object3d = t => Object3d.v(module(T), t)
}

// Let's make some As and Bs and do stuff.

let a1 = A.default()
let b1 = B.default()

let a2 = {
  {A.matrix: M4.default(), children: [A.to_object3d(a1), B.to_object3d(b1)]}
}

let b2 = {
  {B.matrix: M4.default(), children: [A.to_object3d(a2)]}
}

Js.log("Gonna iterate on all objects")
[
  A.to_object3d(a1),
  B.to_object3d(b1),
  A.to_object3d(a1),
  B.to_object3d(b2),
]->Js.Array2.forEach((Object3d.Object3d(module(M), object3d)) =>
  M.foo(object3d)
)

Js.log("Gonna iterate children of b2")
b2
->B.children
->Js.Array2.forEach((Object3d.Object3d(module(M), object3d)) => M.bar(object3d))

If you ran that you would get something like this:

$ node example.bs.js 
Gonna iterate on all objects
You called foo from A module
You called foo from B module
You called foo from A module
You called foo from B module
Gonna iterate children of b2
You called bar from A module

These threads posts have some solutions to your problem (but most in OCaml…though you can find examples of using these techniques in this forum).

Now depending on what you’re trying to do and your constraints some may be better than others. Eg the ADTs are simpler but you may run into the expression problem if you have to add a lot of types.

3 Likes

This type ext is sealed, it can not be extended by a user.

That’s true if you want to lose control of your data this won’t help you.

That is fascinating, thank you! I’ll need some time to digest and somewhat understand this.

1 Like

After playing around a bit with polymorphic variants I ended up with something that ‘feels’ quite nice. Posting this for history if anyone else might find it useful. First class modules seem a bit too complicated, although I’m still definitely very curious, I feel like in a year I won’t be able to understand them again :smiley:

  type mesh = {geometry: int}
  type camera = {fov: int}

  // I need to define all possible types here but oh well
  type kinds = [#mesh(mesh) | #camera(camera)]

  // No idea how I got here but I like it
  type rec someObject<'a> = {kind: 'a, matrix: int, children: array<object3d>}
  and object3d = someObject<kinds>

  // These functions work on any kind of objects
  let updateMatrix = someNode => {
    someNode.matrix
  }
  let add = (someNode, other) => {
    someNode.children->Array.push(other)
  }

  // This type is someObject<[#mesh(mesh)]>, i.e. I can narrow the type to a concrete 'kind', really nice
  let someMesh = {kind: #mesh({geometry: 2}), matrix: 2, children: []}

  // Back to 'wider' type
  someMesh->add((someOtherMesh :> object3d))

  updateMatrix(someMesh)->ignore
2 Likes