Nameof operator

Does ReScript have the concept of a nameof operator? Example in F#: Nameof - F# | Microsoft Learn

Something that can print the current value or field name of a symbol.

I’m my code I need to do something like:

type order = {
  isProcessed: bool,
  contact: contactPerson,
  created: milliseconds,
  tickets: array<ticket>,
  food: foodCount,
}

await orderSnapshot->update_field("isProcessed", true)

To update a single value in my Firebase database, I need to pass the name of my field as a string.
It would be great if I could do something like await orderSnapshot->update_field(nameof(isProcessed), true)

Does something like this exist?

There’s no such operator in rescript. You can build a runtime helper for this using proxy:

await orderSnapshot->update_field(o => o.isProcessed, true)

Where o is a proxy used to track which field is accessed. Also, this way, you can infer the value type.

1 Like

What would that proxy look like?

I’m talking about javascript proxy with get trap

So something along these lines:

const handler = {
  get: function (target, property, receiver) {
    return property;
  },
};

let nameof = (instance, getProperty) => {
  const p = new Proxy(instance, handler);
  return getProperty(p);
};

// Example property access
let y = { bar: 3 }
console.log(nameof(y, (y) => y.bar));

?

That’s very clever, had never thought of that.

I don’t remember the exact API. Looks like the code won’t work correctly, but this is what I meant :+1:

I like hacky solutions like this :joy:

2 Likes

This works for me: ReScript Playground

3 Likes

Some time later, I’m not super excited anymore about this solution. My nameof operator, it always needs a type annotation like nameof((o: Domain.Firebase.order) => o.isProcessed) and I don’t find it very ergonomic.

I’m wondering if ppx would not be convenient to generate string constants of my fields instead.

%something
type foo = {
   x: int,
   y: string,
   bar: array<string>
}

and generate

// Not sure about casing, but you get the idea
module FooKeys = {
   let X = "x"
   let Y = "y"
   let BAR = "bar"
}

Would such a PPX thing be possible in ReScript? I would guess, AST wise it is not that hard to construct.

I can understand this. Accessing by keys rarely happens in pure ReScript code, but is useful when interacting with external JS codes.

I used this pattenrs a lot

type key =
  | @as("preferred_language") PreferredLanguage
  | @as("preferred_timezone") PreferredTimeZone

@genType
module Key = {
  let \"PreferredLanguage" = PreferredLanguage
  let \"PreferredTimeZone" = PreferredTimeZone
}

Used it for building DTOs

There may some community PPXs exist already. e.g. GitHub - Astrocoders/lenses-ppx: GADT lenses

But generalizing it with using PPX or Proxies seems like an overkill solution. What it really need is more of a simple record ↔ dict converter.

My latest shot at this is by using the rescript-tools doc command. (from @rescript/tools)
In a simple script I get the json ast from a file, traverse the nodes a bit and generate some simple code.

open Tools_Docgen

@scope(("import", "meta")) @val external importMetaDir: string = "dir"

let currentDir = Path.resolve([importMetaDir])
let domainFile = Path.join([currentDir, "..", "functions", "src", "Domain.res"])
let json = await (sh`bun rescript-tools doc ${domainFile}`)->ShellPromise.json
let doc = decodeFromJson(json)

let domainFirebase = doc.items->Array.find(item => {
  switch item {
  | Module({id: "Domain.Firebase"}) => true
  | _ => false
  }
})

let records = switch domainFirebase {
| Some(Module({items})) =>
  items->ArrayX.choose(item => {
    switch item {
    | Type({name, detail: Record({items: fields})}) => {
        let fieldNames = fields->Array.map(f => f.name)
        Some(name, fieldNames)
      }
    | _ => None
    }
  })
| _ => []
}

let keysFile = Path.join([Path.dirname(domainFile), "Keys.res"])

let uppercase = (value: string) => {
  let capital = value->String.substring(~start=0, ~end=1)->String.toUpperCase
  let rest = String.substringToEnd(~start=1, value)

  `${capital}${rest}`
}

let contents =
  records
  ->Array.map(((name, properties)) => {
    let keys = properties->Array.map(p => `let ${p} = "${p}"`)->Array.join("\n")

    `module ${uppercase(name)} = {
  ${keys}
  }`
  })
  ->Array.join("\n\n")
  ->Bun.Write.Input.fromString

let _ = await Bun.Write.write(
  ~destination=Bun.Write.Destination.fromPath(keysFile),
  ~input=contents,
)

let _ = await sh`bun rescript format ${keysFile}`
Console.log("Keys.res was generated")

3 Likes