Generic test for a type variant

Hello, maybe I’m stuck on something simple and I’m not seeing the right approach, but I’m trying to do the following:

Given a value with a custom type, check if it is of a certain variant, with the variant to check being parametric.

Ideally, it would be something like this non working code:

type Placeholder = Foo | Bar Int | Baz String

isPlaceholder Foo (Bar 42) -- False, (Bar 42) is not a Foo
isPlaceholder Bar (Bar 42) -- True, (Bar 42) is a Bar
isPlaceholder Baz (Baz "baz") -- True, (Baz "baz") is a Baz

the idea is that I pass the variant as a constructor function, not as as a value, and the isPlaceholder function checks whether the second argument is of the type of that variant.

The ability to pass the constructor function instead of a value is just for practicality, it would be fine even if I had to call it as isPlaceholder (Bar 0) (Bar 42).

Now, I have a solution which is hand-crafted, but it’s sub-optimal since I need to update it and keep in sync with every variant added to the type:

type Placeholder = Foo | Bar Int | Baz String

isPlaceholder p v =
  case p of
    Foo -> 
      case v of
        Foo -> True
        _ -> False
    Bar _ ->
      case v of
        Bar _ -> True
        _ -> False
    Baz _ ->
      case v of
        Baz _ -> True
        _ -> False

isPlaceholder Foo (Bar 42) -- False
isPlaceholder (Bar 0) (Bar 42) -- True
isPlaceholder (Baz "") (Baz "baz") -- True

I am not even sure how to represent the type of a variant constructor, it would be an n-ary/variadic argument function.

Am I missing something?

Your solution is the correct one :slight_smile:

When you add a new variant, you’ll get a compilation error because the outer case isn’t exhaustive. Then you (or maybe GitHub Copilot?) just pop the new stuff in and you’re done.

Another solution could be to map both operands to a canonical value and then test equality. You’d still get the nudge from the compiler if variants change, but you’d avoid the nested case expressions and the _ -> wildcard branches

toCanonical : Placeholder -> Placeholder
toCanonical val =
  case val of
    Foo -> Foo
    Bar _ -> Bar 1
    Baz _ -> Baz ""

isPlaceholder : Placeholder -> Placeholder -> Bool
isPlaceholder a b =
  toCanonical a == toCanonical b

Thanks to both!

@sam1729 your solution is interesting: I was worried that it might fall short when there are functions inside variants, but apparently when using identity, that is not an issue:

type Placeholder = Foo | Bar Int | Baz (String -> String)

toCanonical : Placeholder -> Placeholder
toCanonical val =
  case val of
    Foo -> Foo
    Bar _ -> Bar 1
    Baz _ -> Baz identity

isPlaceholder : Placeholder -> Placeholder -> Bool
isPlaceholder a b =
  toCanonical a == toCanonical b

isPlaceholder (Baz  (\x -> "foo")) (Baz (\x -> x)) -- Works!

I’m surprised because the manual is clear that (==) cannot be used to compare functions, but apparently elm is happy to perform identity == identity, while it is not for (\x -> "") == (\x -> "")… But that’s another issue.

The JavaScript function that implements Elm’s == operator has an optimization at the start: core/src/Elm/Kernel/Utils.js at 65cea00afa0de03d7dda0487d964a305fc3d58e3 · elm/core · GitHub

If the values compared are === equal in JavaScript, then they are considered equal in Elm. When you refer to identity twice, you refer to the exact same value in JavaScript, which are then === equal. You could also pass in String.toUpper or some other String -> String function, as long as you pass something that happens to be reference equal on the JavaScript side.

Without that optimization, the program would crash: core/src/Elm/Kernel/Utils.js at 65cea00afa0de03d7dda0487d964a305fc3d58e3 · elm/core · GitHub

I wonder if some kind of constructor introspection would be a worthwhile feature to add to the Elm language? Every time I needed to do something like this I always found a different solution, but it still feels like a tempting language feature to want. What would it even look like?