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.
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
@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.
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.
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?