How to abstract common behavior

Suppose there are 2 2D shapes, Rect and Circle

# Rect.res

type t = {
  width: float,
  height: float,
  x: float,
  y: float,
}

let make = (width, height, ~x=0.0, ~y=0.0, ()) => {
  {
    width: width,
    height: height,
    x: x,
    y: y,
  }
}
# Cricle.res

type t = {
  radius: float,
  x: float,
  y: float,
}

let make = (radius, ~x=0.0, ~y=0.0, ()) => {
  {
    radius: radius,
    x: x,
    y: y,
  }
}

then, I want to define a common behavior that can move these two shapes

# Action.res

let move = (t, x, y) => {
  {
    ...t,
    x: x,
    y: y,
  }
}

but I got an error

The record field x can't be found.

If it's defined in another module or file, bring it into scope by:
- Prefixing it with said module name: TheModule.x
- Or specifying its type: let theValue: TheModule.theType = {x: VALUE}


I have experience in TypeScript development, but no ML related.

I hope I can learn some ways to get started from here, and it is better to have some small apps to provide learning.

:handshake: thanks

One way would be to create a variant type for the shape and create a move function
PlayGround

type t =
  | Rectangle({width: float, height: float, x: float, y: float})
  | Circle({radius: float, x: float, y: float})

let move = (shape, x, y) => {
  switch shape {
  | Rectangle({width, height}) =>
    Rectangle({width: width, height: height, x: x, y: y})
  | Circle({radius}) => Circle({radius: radius, x: x, y: y})
  }
}

The reason for the above error is that Typescript is a structural type system and ReScript is a nominal type system.

1 Like

Does this mean how many shapes I have and how many matches will there be? If this is the case, it is very troublesome.:scream_cat:

Note that the error message you are facing is telling that the compiler couldn’t find a type that maches the return type of your function in current scope (rescript doesn’t look in other modules by default).
You can help the compiler via opening module which contains that type open Circle, annotating the function let move: (Circle.t ,float, float) => Circle.t = ... or hint with prefixing in record

let move = (t, x, y) => {
  {
    ...t,
    Circle.x: x,
    y: y,
  }
}

but as you see this doesn’t solve your main problem.
record in rescript are nominally typed, type checker resolves the type of the record by finding that single type declration of record.

These kind of abstractions are usually solved using variants in rescript, for this case I would go with @a-c-sreedhar-reddy 's solution either via inline records or explicit record annotation.

type shape = Circle(Circle.t) | Rect(Rect.t)

1 Like

Thank you very much for your reply,

I know some simple concepts of nominally and duck type.

So what does the best practice look like for such a problem?

Do you provide a move method for each type?

To share record fields between different record types, it’s usually easiest to add a level of indirection by nesting them. Example:

type shape = Circle({radius: int}) | Rectangle({height: int, width: int})
type t = {shape: shape, x: int, y: int}
let move = (t, ~x, ~y) => {...t, x: x, y: y} // move doesn't require a switch
let area = ({shape, _}) =>
  switch shape {
  | Circle({radius}) => Js.Math._PI *. Belt.Int.toFloat(radius) ** 2.
  | Rectangle({height, width}) => Belt.Int.toFloat(height * width)
  }
let makeCircle = (~radius, ~x, ~y) => {
  {shape: Circle({radius: radius}), x: x, y: y}
}
let makeRectangle = (~height, ~width, ~x, ~y) => {
  {shape: Rectangle({height: height, width: width}), x: x, y: y}
}
8 Likes

Based on your suggestion, I changed it to the following code. Does it make sense?

# Shape.res

type t =
  | Rectangle({width: float, height: float})
  | Circle({radius: float})
  | Triangle({height: float, base: float})

let area = shape => {
  switch shape {
  | Rectangle(rect) => rect.width *. rect.height
  | Circle(circle) => circle.radius ** 2.0 *. 3.141592653589793
  | Triangle(triangle) => triangle.height *. triangle.base *. 0.5
  }
}

# Coordinate.res

type t = (float, float)

# Object.res

type t = {
  shape: Shape.t,
  position: Coordinate.t,
}


let move = (obj: Object.t, position: Coordinate.t) => {
  {
    ...obj,
    position: position,
  }
}

6 Likes

That looks like a good design, IMO. Interestingly, a discussion about this exact problem happened on the OCaml forum recently. They have some clever solutions (although I would argue that some of them are a bit too clever to be practical). It’s interesting to see that there are different approaches you can take.

4 Likes