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.

1 Like

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
1 Like

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

1 Like

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?

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.