polyVariant to string?

Any hints for Polymorphic Variants with record constructors?

type context = { x: int }
type states = [
  | #Idle(context)
  | #Active(context)
]

let state = #Idle({ x: 1 })
let nextState = #Active({ x: 1 })

Js.Console.log("Transitioned from " ++ (state :> string) ++ " to  " ++ (nextState :> string))

Results in:

Type [#Dragging(context) | #Idle(context)] is not a subtype of string

What I’ve tried:

  • Reading the docs on polymorphic variants, which covers using (#blue :> string), as well as this post
  • It also mentions that record constructors have a NAME attribute that’s a string, which is true looking at the .bs.js
  • state[“NAME”] was also rejected
  • (state :> context), (state :> #Idle{x: int}), (state :> {x: int}), and (state :> {..}) on their own and they also did not work for me.

I suspect it’s not possible to coerce poly vars with values. You may need to handle it more explicity:

let contextToString = ({x}) => `(x=${Js.Int.toString(x)})`

let stateToString = state => {
  switch state {
  | #Idle(context) => `Idle${contextToString(context)}`
  | #Active(context) => `Active${contextToString(context)}`
  }
}

Js.Console.log("Transitioned from " ++ stateToString(state) ++ " to " ++ stateToString(nextState))

// Transitioned from Idle(x=1) to Active(x=1)

Or if you are not too concerned about the string representations and happy with whatever data structure ReScript chooses, you could convert to a JSON string:

external asJson: 'a => Js.Json.t = "%identity"
let asJsonString = x => asJson(x)->Js.Json.stringify

Js.Console.log("Transitioned from " ++ asJsonString(state) ++ " to " ++ asJsonString(state))

// Transitioned from {"NAME":"Idle","VAL":{"x":1}} to {"NAME":"Idle","VAL":{"x":1}}

However this is a bit hacky and probably not a good idea for important code.

1 Like

That’s helpful but what if I just want the string keyword of the state like “Idle” or “Active”? I can write that function with the switch for now but can’t help but think there is a better way I’m not seeing given that it already outputs to an object with a NAME prop.

Yeah, writing a function with a switch expression is basically the way to do it.

EDIT: another way is to realize that, if you think of your type as an ‘algebraic data type’, then:

type state = [
| #Idle(context)
| #Active(context)
]

Is the same as

type state = ([#Idle | #Active], context)

I.e., a pair of (either ‘Idle’ or ‘Active’, context). And when expressed this way, it’s automatically coerced into a string:

let (status, context) = (#Idle, {x: 1})
let (nextStatus, _) = (#Active, context)
Js.log4("Transitioned from", status, " to ", nextStatus)
3 Likes

That makes sense and is how I am thinking of it which is why I was thinking there HAD to be another way to get out the state key value into a string without manually pattern matching it to one.

Anyway I found myself with a lot of:

switch state {
| #Idle(context) => context.pos *. 100.0
| #Active(context) => context.pos *. 100.0
}

And a few other structures where I’m ignoring the context in all cases to derive a value based on state. This got me thinking it might be better to invert the shape:

type state = [
| #Idle
| #Active
]
type context = {
  x: int,
  y: int,
}
type machine = {
  state: state,
  context: context,
}

Now (machine.state :> string) can be used, and state agnostic where it makes sense.

2 Likes

This was really helpful. Thank you.

Side note, but since we’re on the subject - in v11 you’ll be able to do the same for regular variants, now that they’re also represented by strings at runtime:

type myType = One | @as("two") Two

Console.log(One :> string) // "One"
Console.log(Two :> string) // "two"

This also works for float and int if the entire variant is represented by either via @as:

// Has only ints
type myInt = | @as(1) On | @as(0) Off

Console.log(On :> int) // 1

// Has only floats
type myFloat = | @as(1.5) Low | @as(2.1) Mid | @as(3.5) High

Console.log(Low :> float) // 1.5
7 Likes

This will be great for interop.

3 Likes