Checking a variant's tag without a match?

Is there a good way to check if a variable is a variant with a particular tag without doing a match? I think the answer is no.

My motivation is this: I want to write a small function mustbe a b that confirms that the variant b has the tag given in a, as in

mustbe Float x

or some such. However, I don’t think this is possible, especially if the variants have associated data.

(Having this function would save me a bunch of repeated code in some ad hoc recursive descent parsing I’m doing, but I’m guessing that I can’t avoid it.)

Polymorphic variants might fit your case. They’re a lot more flexible; in fact you don’t have to declare them beforehand because OCaml infers them for you from the usage.

I want to avoid them for now. I won’t be able to tell as easily if I’ve missed a case in pattern matching etc.

However, even if I used them, wouldn’t I have the same problem? I’m not sure what syntax would accomplish what I’m trying to do, even with a polymorphic variant.

Ah, I see, I misread your question originally. Yeah, to do what you want would entail introspecting the given value to get its type, checking if that is a variant type, and then checking if the variant type has the given case. So, you’d need either runtime reflection … or maybe compile-time macros? Not too sure :slight_smile:

This function might be a little bit nicer in SML, where constructors are first-class functions that can be passed around by themselves, but in OCaml you can do this:

let sametag (a : 'a) (b : 'a) = Obj.tag (Obj.repr a) = Obj.tag (Obj.repr b)

With the type annotations there to prevent it from settling on 'a -> 'b -> bool, which can return true for different types since tag values start at 0 for all variants.

So to test that an int option value is at least a Some _ rather than a None:

if sametag (Some 42) suspect_value then "same" else "different"

Where the 42 is not meaningful except to help provide the reference int option with the desired tag. Naturally sametag (Some 1) (Some 2) is true.

4 Likes

Thank you, I would never have guessed that was possible at all!

I’d seen the Obj module before, but the documentation for it is really scant. There’s no documentation on most of the functions (none on repr or tag), and the only comment for the whole thing is “Operations on internal representations of values. Not for the casual user.”

I gather that in SML you would simply compare the equality of the two constructor functions? Of course, even if this were the case in OCaml, = on functions gives you Invalid_argument, right?

Real World OCaml covers some Obj functions.

SML also can’t compare constructors for equality, but at least the basic interface of passing SOME rather than (Some 42) is possible. I imagined it might be possible to compare equality of the two objects after applying the constructor to the second object’s contents somehow, or to use some implementation’s extension to finish the job.

Oops, sorry. sametag is actually wrong.

# type x = A | B | C | D | E;;
# sametag C D;;
- : bool = true

Meanwhile,

# let x : int = Obj.magic D;;
val x : int = 3
# let x : int = Obj.magic A;;
val x : int = 0

It won’t do what you want though, to apply Obj.magic to (Some 42). sametag is probably still possible, but you’d need to figure out what you’re looking at, and then make the appropriate comparison. Probably Obj.tag would be more helpful for that ‘figure out’ part.

1 Like

Ah, a shame. :frowning:

My code is much messier than I’d like as a result of this, but perhaps I can think overnight of a different solution for doing what I need by structuring my code differently in the first place.

I believe you can approximate this with Jane Street’s ppx_variants_conv:

open Base

type t =
  | Char of char
  | Float of float
  | Bool of bool [@@deriving variants]

The module Variants now contains a value for each constructor, from which we can access its name as a string and its rank (an integer corresponding to the declaration order). Now we can test by comparing rank:

let mustbe tag x =
  tag.Variant.rank = Variants.to_rank x

let () =
  assert (mustbe Variants.char (Char 'c'));
  assert (not (mustbe Variants.float (Char 'c')))

However, this isn’t type safe, as we can just as easily pass in constructors for other types:

type u =
  | Foo
  | Bar
[@@deriving variants]

This will pass, where we’d like it to typerror or at least fail the assertion:

let () =
  assert (mustbe Variants_of_u.foo (Char 'c'))
4 Likes

techate’s sametag can be repaired somewhat:

let same_constructor a b =
  let a_repr = Obj.repr a in
  let b_repr = Obj.repr b in
  match Obj.is_int a_repr, Obj.is_int b_repr with
  | true, true -> a = b
  | false, false -> Obj.tag a_repr = Obj.tag b_repr
  | _, _ -> false

This is nasty code that makes assumptions about the implementation. I would not expect that there is any guarantee about its behaviour in future versions of OCaml, either.

Try the ppx suggestion first, as that is far cleaner.

I gather that in SML you would simply compare the equality of the two constructor functions?

Functions are not eqtypes in SML, so such a comparison would not pass the type checker.

2 Likes

Are you using a parser combinator library or Menhir currently? If not, those might be more straightforward.

Well, to explain, I’m using Menhir for my main project, but I wanted to knock off a small task with a primitive recursive descent + parser combinator hack of my own.

I wanted a family of functions to recognize individual tokens, but The Right Way to do those seemed clearly to be to Curry something like mustbe (or sametag or whatever you want to call it) instead of writing each afresh. But sadly, there doesn’t seem to be a good way to do that. :frowning:

BTW, just as a pure aside (it has no real impact on the current discussion at all), why can’t functions be compared for equality? Clearly you can’t tell that two distinct functions that do the same thing are equal, but you can tell if two functions are identical, can’t you?

Oh, and as another aside: I’m finding the modern “easy” way to do error messages for Menhir not very easy at all. The explanations in the manual may be at fault. :frowning:

You can compare functions for being identical (==). You just can’t compare for equality (=). But you might not get the answer you would want due to inlining and such.

# sin == sin;;
- : bool = false
# let f x = x + 4;;
val f : int -> int = <fun>
# f == f;;
- : bool = true

Since functions are immutable, the meaning of (==) is very weak for them. Bordering on not useful at all.

1 Like

I got a little bored, so I decided to try my hand at writing a ppx rewriter for this. I haven’t bothered packaging it yet, and feedback is quite welcome, but it works like [%constr_test Some __] (Some 1).

And a huge shout-out to @rgrinberg for this article, which was invaluable in understanding the ppx ecosystem.

5 Likes

As I understand it, it’s because a function is more than its definition, it also contains its closure, so what you would really need is a way of comparing closures.

Ah, this just made me remember something possibly from Prof. Dan Grossman’s Programming Languages course. In a closure, a lot of type information is actually hidden. E.g.:

let message = "Hi"

let f test = if test then print_endline message

Here f has type bool -> unit, but in reality its closure also contains a string and that type information is hidden. So really any function type may contain a myriad of hidden types.

So from the type of a function, you can’t figure out exactly what types of comparisons you’ll need to do, because you don’t know what hidden types the closure contains.

But f in this case is probably represented as some address in memory, and if I compare that to the address in memory of any other function, it will be different, and if the other function has the same address, even if I don’t know its type details, I can be pretty sure it is the same function. Or am I completely off?