so the current design for pattern matching on named constants is that if I'm in a module named Foo, I can do this:
when something is
Foo.pi -> ...
this works in the sense that:
pi -> ... (lowercase names in pattern matches are always variables, just like other -> ...)Num.pi -> ... does what you'd guess it doeshowever, there are some downsides:
app modules)arg -> ... but you also can't do Foo.arg -> because arg isn't top-level)to be clear, there's a workaround for all of these scenarios: you can always use anif guard like pi if pi == Num.pi -> - but it makes code a lot less nice to read if you're trying to match on a bunch of constants
e.g. HTTP status codes
so I'd like to explore ideas for other ways this could work
one idea:
when something is
@pi -> ...
@ followed by a lowercase letter is not currently in use, because @ is only used for opaque type wrappers.
also, this would be syntactically unambiguous too:
when something is
@Num.pi -> ...
...because opaque type wrappers never have a dot in them
this syntax would address all the concerns above, but:
@, which currently has only one meaning (opaque types) and that one meaning is unrelated to this oneanyone have thoughts on this idea or alternative ideas for what we could do here?
Could we not decide dynamically based on if pi is in scope or not?
If pi is in scope, compare with the value of pi, if it is not, perform the current behavior?
What about “.pi -> …” instead of “Num.pi -> …”?
that's a possibility, although at least today, .pi already specifically means something else (namely, it's sugar for \record -> record.pi)
but it would be syntactically unambiguous in a pattern match
Anton said:
Could we not decide dynamically based on if pi is in scope or not?
If pi is in scope, compare with the value of pi, if it is not, perform the current behavior?
I think that's possible, although I can imagine some confusing scenarios with that:
when num is
0 -> 1
pi -> pi + 1
this would compile whether or not pi is in scope, and if you introduced pi to scope it would silently do something different
Yeah, I think it's a balance between these risks and a custom symbol that is hard to discover for users
yeah the discoverability on that option is pretty hard to beat :big_smile:
one idea: if you're in exactly the following scenario, we give a warning:
pi appears in the patternpi also appears in the body of the expressionpi also is a constant that's in scopebecause if it only appears in the pattern, but not in the body after the ->, then it's either that you're trying to refer to a constant or else you'd be getting an unused name warning (that would tell you to use an _)
and if it appears in the pattern and the body, but not in the enclosing scope, then it's unambiguously trying to be used in the only way it's currently supported
so then:
pi -> # code that doesn't reference pi is pattern matching on a constant; if there's no constant in scope, we (continue to) give a warning for unused name, although in this situation we'd probably modify that warning to say "if you meant to reference a constant, I don't see one in scope"pi -> pi + 1 continues to work as long as pi is not in scopepi -> pi + 1 gives a warning if pi is already in scope (I guess shadowing?)that's very interesting - perhaps surprising, but also if I look at the code in context, I think I can always guess what it's doing.
and if it gives a compiler warning in the one scenario where a behavior change would be possible, then it doesn't seem error prone.
one downside of this design is that if you really actually do want to match on pi (the constant) and then use it in the body, you have to write something silly like:
pi = 3.14159
piAgain = pi
when num is
pi -> piAgain + 1
but honestly this seems like such a narrow edge case that I really don't mind it being weird.
I'm open to that design...what do others think?
I think that would be very confusing if you made a typo, though I guess you would get an unused variable warning. And I don’t like that you would need context to understand what the pattern match does
I think Elixir uses ^ for this: https://elixir-lang.org/getting-started/pattern-matching.html#the-pin-operator
so that idea is basically the same as the @ idea but with a different sigil?
Yeah
a random thought about discoverability: supposing we did introduce special syntax for this, if you tried to write pi -> ... and pi was already in scope, you'd already get a shadowing error for this today - and in that error message, we could suggest "hey if you want to use the pi that's in scope, here's how to do that:" and then show the syntax
so it actually might be about as discoverable after all
a neat thing about ^pi -> is that it kinda reads like :point_up:pi -> - like, "that pi up there in the outer scope"
What if we flip it around?
Do pi -> ... for constants and @pi -> for binding to a variable?
I think beginners would first try out checking for constants, and binding is something they have to learn. Not sure about that though
so then it would be like Ok @val ->?
Yeah. It makes the more common case more verbose though
So not sure about it, just a thought
This kinda reminds me of how in JS with objects you can have dynamic keys with [varname]:
const a = 'abc';
const b = {
a: 5,
[a]: 10,
}
console.log(b) // Object { a: 5, abc: 10 }
I think based on that this concept will be familiar with a lot of users. I like changing it to explicitly bind with @ as well even though that is the more common case. I personally found Ok val confusing in pattern matching when I first encountered it since I didn't understand where val was coming from.
Looking at some example code I think that making the common case more verbose would get annoying quickly.
I like ^pi ->
Can you call functions with variable arguments in there?
y = 1
z = 2
when x is
someFn y z -> ...
Currently, starting your expression to match with a function does not work
(someFn y z) also does not work
To me this is one of those cases where for more advanced users it is great, but for people just looking at roc and debating learning it is another turn off that makes it likely many people will just skip roc due to looking to weird (though there may be no way around that due to backpassing being so confusing if just glancing at roc)
@Brendan Hansknecht what design do you think is best?
fwiw I find the "implicitly use a constant in scope" option really confusing in semantics, even if a warning is enabled for it - the syntactic ambiguity just doesn't seem worth it. I think it's worth noting the common case you'd use this, which in my mind is when you are comparing an opaque type (or non-semantic type, like ints for HTTP status codes) against a constant value. In those cases, the constant value is very likely at the top-level of a module scope, and typically in a library. With that in mind, I personally would strongly prefer the "Module.id" option or a sigil-based option. In the "Module.id" option, if you are in an app module or a nested scope and need to do "_ if condition == id -> ..", that doesn't seem unreasonable, because my claim is that is the uncommon case.
In my comment, I was talking about ^foo -> syntax. Overall, I think I agree with Ayaz's sentiment.
yeah, although an annoying thing about the Module.id -> option is that e.g. app modules don't have names
(and they aren't - and shouldn't be - importable, so it's weird to give them a name just for this)
right, but how often are you defining constants like that in an app module? I would argue that not very often - when you are it's typically in an interface from a library you import
fair
so I guess there the idea is basically: keep the current design of Foo.pi -> being the only way to do this, because:
app modules and for constants defined in nested scopes) are so uncommon that it's fine if the only way to express those patterns is _ if something == pi ->I think that makes sense. Maybe you could even argue it’s an advantage to know that right now, patterns always consist completely of constant values (as opposed to variables in nested scopes that can be different on different invocations).
yeah. I think the sigil version is also reasonable, but I suspect that the existing model will cover 90-95% of use cases
here is some evidence for this: in the Roc compiler codebase, there are 254 instances of the Module::Id pattern-matching case. There are 32 instances of _ if ... -> pattern matches, none of which are solely of the form _ if matchCondition == someConstant.
Foo.pi is definitely more intuitive than @pi or ^pi. My experience has been that you rarely need the pin operator in elixir, so beginners can get extra tripped up because they first see it after they think they've figured out the core syntax. When I do use the pin operator I tend to leave a comment explaining why, so that's not a great endorsement.
Kasper Møller Andersen said:
As for constants, how does Roc plan to let you match against constants? As in, can I do something like:
MY_CONSTANT = "constant" ... where myString is MY_CONSTANT -> ... "someConstantString" -> ... ...In Scala, an upper case first letter allows you to match against a value like this. That's quite subtle in practice, so I wouldn't recommend it necessarily, but it is a potential use case at least.
the design (not currently implemented) is to let you qualify them, e.g.
where foo is
MyModule.constant -> ...
(and you can fully-qualify using the current module's name to get constants defined in the same module)
Richard Feldman said:
Kasper Møller Andersen said:
As for constants, how does Roc plan to let you match against constants? As in, can I do something like:
MY_CONSTANT = "constant" ... where myString is MY_CONSTANT -> ... "someConstantString" -> ... ...In Scala, an upper case first letter allows you to match against a value like this. That's quite subtle in practice, so I wouldn't recommend it necessarily, but it is a potential use case at least.
the design (not currently implemented) is to let you qualify them, e.g.
where foo is MyModule.constant -> ...(and you can fully-qualify using the current module's name to get constants defined in the same module)
What if you wanted to match against a constant in a single file app? Then there is no module name to use, but I still think this might come up sometimes
Unless you're looking for something deeper than equality, I believe that already works with myConstant since every def is constant and every Eq-able type is matchable.
JanCVanB said:
Unless you're looking for something deeper than equality, I believe that already works with
myConstantsince every def is constant and every Eq-able type is matchable.
It wouldn't I think? It would just match everything (same as underscore). The only difference is that is would actually bind the thing to the variable. You would get a shadow error if the name is bound somewhere already, and a variable not used error if it isn't.
JanCVanB said:
Kilian Vounckx said:
JanCVanB said:
Unless you're looking for something deeper than equality, I believe that already works with
myConstantsince every def is constant and every Eq-able type is matchable.It wouldn't I think? It would just match everything (same as underscore). The only difference is that is would actually bind the thing to the variable. You would get a shadow error if the name is bound somewhere already, and a variable not used error if it isn't.
Hmm, I don't understand what you're communicating here, so I infer that it involves a deep concept that I haven't perceived before. Hopefully someone more experienced can answer your question.
I'll give an example. Hopefully that helps.
The following code will (and should) give a compiler warning:
someConst = 42
foo = 42
when foo is
someConst -> {}
_ -> {}
The first branch will match anything, because the value just gets locally bound to the local variable someConst within the branch. The same as if you would have done:
when someRecord is
{ fieldName: someConst } -> {}
_ -> {}
Here, the field in someRecord gets bound to the name someConst. It will not match the value of any constant in an outer scope with the same name. The same happens in the first example.
Now because roc does not allow shadowing, there will be a compile error saying that the value gets shadowed, which means this bug won't happen. But it also means it is impossible to match against a constant which is not defined within a module.
Yeah, not supported in current roc, but the plan is to require the module prefix.
someConst = 42
foo = 42
when foo is
WhateverCurrentModule.someConst -> {}
_ -> {}
Oh wow I thought pattern matching against a variable name worked today. Yes please!
It doesn't cause it creates a new variable
when foo is
someConst -> #someConst is a variable with the value of foo.
_ -> {}
Why not just resolve someConst? If we don't allow shadowing anyways, it would just be a compiler error today instead of potentially more concise syntax for a seemingly common switch case pattern.
answer = 2 + 2
Stdout.line! "What is 2+2?"
when Stdin.line! is
answer -> Stdout.line! "Correct!"
_ -> Stdout.line! "Incorrect."
(typed on mobile, ignoring Num casting)
CurrentModule.answer seems verbose.
We can move this discussion to another topic, though.
Also, "Why not...?" spoken verrrry casually as Not The Person Who Would Implement This.
JanCVanB said:
Why not just resolve someConst?
It is more bug prone. If someone adds a new variable or renames a variable before your when ... is it will silently convert from capturing a variable to matching against a constant.
Also, yeah, we have really driven into an unrelated tangent. We should move all of this discussion on constant matching to a different thread.
I'm gonna move messages, sorry if it is a mess for a minute, they messages are interspersed.
A message was moved here from #ideas > snake_case instead of camelCase by Brendan Hansknecht.
Elixir solves this with the ^ pin operator.
Explanation: https://elixirschool.com/en/lessons/basics/pattern_matching, though that article fails to mention that it's biggest use is in case (when) like the motivating examples here.
of note, you can always do _ if foo == constant -> ...
or Ok foo if foo == constant -> ... etc.
so allowing module-qualified constants is essentially syntax sugar to save you from writing the if
Theoretically, we could make it optimize better as well. I guess we could optimize the if better, but that is more brittle
Last updated: Jun 16 2026 at 16:19 UTC