Strip variants from output

Is there some way to strip variants from output, especially like from nested Records?

type contact = Contact({ number: string, country: string });
type person = Person({ name: string, contact: contact, country: string });

The output of person would have a Variant wrapping the contact field.
My reasoning here is to use Variants instead of modules for lightweight distinct record types. Should I just use modules?

When you say ‘output’, what do you mean? Like printing to a string or JSON? You want to convert a person value to a string and have it show up as something like { "name": "A", "contact": { "number": "1", "country": ...?

Actually I figured it out that using unboxed variants (@ocaml.unboxed) would remove the variant representation in the JS output, which was what I was aiming for.

2 Likes

Just @unboxed should be enough.
Also, ReScript records are already typed nominally, the only problem, afaik, is that sometimes the compiler derives the wrong record type from the first field used. But you can disambiguate with module/type annotations.

3 Likes

I think @unboxed works. But you could also use:

external unwrap: 'a => 'b = "%identity"

and do something like this. I must say that this should be used with caution since you’re removing your type constraints. Usually you should use this when you need to send a variable to the JS-side by removing the variants from the output.

I’m a bit confused on what you mean, because your example doesn’t include any extra output that could be removed.

type contact = Contact({number: string, country: string})
type person = Person({name: string, contact: contact, country: string})

let a = Person({
  name: "bob",
  contact: Contact({number: "555", country: "usa"}),
  country: "usa",
})

Output:

var a = /* Person */{
  name: "bob",
  contact: /* Contact */{
    number: "555",
    country: "usa"
  },
  country: "usa"
};

Playground link.

For variants with only one constructor and an inline record, then the output is already unboxed. (The @unboxed annotation isn’t necessary.)

I’m interested in why you’re using variants as namespaces instead of modules. There’s nothing necessarily wrong with variants, but modules are traditionally the more idiomatic way of doing that. Variants are a bit clunkier. For example, you can’t access record fields like a.name without deconstructing a first.

3 Likes

Ahh yes sorry I didn’t even bother to test with it removed!

I guess I was looking for a syntactically lighter way to declare records with duplicate fields.

@hoichi is it safe to declare the two records naked?

type contact = { number: string, country: string };
type person = 
 { name: string, contact: contact, country: string };

Because I read on stackoverflow “ The Ocaml language requires all fields inside a module to have different names. Otherwise, it won’t be able to infer the type of the below function”

And while the compiler still compiles I’m not sure if this was a rule or a suggestion.

Yes I guess.
eg: In let name = variable.name Rescript will infer the closest record type with name field. If that is not what you wanted to use then you need to provide type for variable manually.

It’s “safe” in the way we typically mean safe. I think the SO answer you mention isn’t worded very well. There is no requirement that records have unique field names within modules. However, the type checker infers records based on their field names, so it will sometimes (but not always) infer a different record than you expected if there are duplicated field names. This is easy to catch when it happens, though, since the compiler will give you a type error. You can correct it by explicitly annotating the type. This is a case where “if it compiles, it works” holds true.

If your main concern is verbose syntax, then I think that using modules as namespaces will be better. Here’s your example using modules:

module Contact = {
  type t = {number: string, country: string}
}
module Person = {
  type t = {name: string, contact: Contact.t, country: string}
}
let bob: Person.t = {
  name: "bob",
  country: "usa",
  contact: {number: "555", country: "usa"},
}
// Alternatively:
let bob = {
  Person.name: "bob",
  country: "usa",
  contact: {number: "555", country: "usa"},
}

We need to annotate bob because its record fields are defined a different module, but we don’t need to annotate the nested contact field because the typechecker already knows what type that needs to be.

The module version will also be less verbose when you’re accessing fields. Records may need to be annotated sometimes with their module name, but only in cases where the compiler can’t infer them. Variants will always need to be destructured to access their contents, though, which can become verbose quickly.

None of this necessarily means you should avoid using variants and inline records, but modules seem to already be built to solve the problem you’re describing.

1 Like