Stream: ideas

Topic: pattern matching on named constants


view this post on Zulip Richard Feldman (Jun 29 2023 at 12:12):

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:

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:13):

however, there are some downsides:

  1. Referring to the module's name inside itself is uncommon, and probably not what people would guess
  2. Some modules don't have names (e.g. app modules)
  3. This doesn't work for variables inside expressions (e.g. if you have a function arg and want to match on that, you can't do arg -> ... but you also can't do Foo.arg -> because arg isn't top-level)

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:14):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:15):

e.g. HTTP status codes

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:15):

so I'd like to explore ideas for other ways this could work

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:16):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:17):

this syntax would address all the concerns above, but:

view this post on Zulip Richard Feldman (Jun 29 2023 at 12:17):

anyone have thoughts on this idea or alternative ideas for what we could do here?

view this post on Zulip Anton (Jun 29 2023 at 12:42):

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?

view this post on Zulip Ayaz Hafiz (Jun 29 2023 at 12:59):

What about “.pi -> …” instead of “Num.pi -> …”?

view this post on Zulip Richard Feldman (Jun 29 2023 at 14:45):

that's a possibility, although at least today, .pi already specifically means something else (namely, it's sugar for \record -> record.pi)

view this post on Zulip Richard Feldman (Jun 29 2023 at 14:45):

but it would be syntactically unambiguous in a pattern match

view this post on Zulip Richard Feldman (Jun 29 2023 at 14:47):

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

view this post on Zulip Anton (Jun 29 2023 at 14:53):

Yeah, I think it's a balance between these risks and a custom symbol that is hard to discover for users

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:02):

yeah the discoverability on that option is pretty hard to beat :big_smile:

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:03):

one idea: if you're in exactly the following scenario, we give a warning:

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:04):

because 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 _)

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:04):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:07):

so then:

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:09):

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.

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:09):

I'm open to that design...what do others think?

view this post on Zulip Ajai Nelson (Jun 29 2023 at 15:17):

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

view this post on Zulip Ajai Nelson (Jun 29 2023 at 15:19):

I think Elixir uses ^ for this: https://elixir-lang.org/getting-started/pattern-matching.html#the-pin-operator

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:27):

so that idea is basically the same as the @ idea but with a different sigil?

view this post on Zulip Ajai Nelson (Jun 29 2023 at 15:29):

Yeah

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:29):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:29):

so it actually might be about as discoverable after all

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:31):

a neat thing about ^pi -> is that it kinda reads like :point_up:pi -> - like, "that pi up there in the outer scope"

view this post on Zulip Kilian Vounckx (Jun 29 2023 at 15:32):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 15:33):

so then it would be like Ok @val ->?

view this post on Zulip Kilian Vounckx (Jun 29 2023 at 15:34):

Yeah. It makes the more common case more verbose though
So not sure about it, just a thought

view this post on Zulip Tommy Graves (Jun 29 2023 at 15:34):

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.

view this post on Zulip Anton (Jun 29 2023 at 15:41):

Looking at some example code I think that making the common case more verbose would get annoying quickly.

view this post on Zulip Anton (Jun 29 2023 at 15:42):

I like ^pi ->

view this post on Zulip Tommy Graves (Jun 29 2023 at 15:51):

Can you call functions with variable arguments in there?

y = 1
z = 2
when x is
  someFn y z -> ...

view this post on Zulip Anton (Jun 29 2023 at 16:02):

Currently, starting your expression to match with a function does not work

view this post on Zulip Anton (Jun 29 2023 at 16:05):

(someFn y z) also does not work

view this post on Zulip Brendan Hansknecht (Jun 29 2023 at 16:12):

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)

view this post on Zulip Richard Feldman (Jun 29 2023 at 16:45):

@Brendan Hansknecht what design do you think is best?

view this post on Zulip Ayaz Hafiz (Jun 29 2023 at 18:15):

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.

view this post on Zulip Brendan Hansknecht (Jun 29 2023 at 18:17):

In my comment, I was talking about ^foo -> syntax. Overall, I think I agree with Ayaz's sentiment.

view this post on Zulip Richard Feldman (Jun 29 2023 at 18:19):

yeah, although an annoying thing about the Module.id -> option is that e.g. app modules don't have names

view this post on Zulip Richard Feldman (Jun 29 2023 at 18:20):

(and they aren't - and shouldn't be - importable, so it's weird to give them a name just for this)

view this post on Zulip Ayaz Hafiz (Jun 29 2023 at 18:20):

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

view this post on Zulip Richard Feldman (Jun 29 2023 at 18:20):

fair

view this post on Zulip Richard Feldman (Jun 29 2023 at 18:23):

so I guess there the idea is basically: keep the current design of Foo.pi -> being the only way to do this, because:

view this post on Zulip Ajai Nelson (Jun 29 2023 at 18:42):

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).

view this post on Zulip Ayaz Hafiz (Jun 29 2023 at 18:58):

yeah. I think the sigil version is also reasonable, but I suspect that the existing model will cover 90-95% of use cases

view this post on Zulip Ayaz Hafiz (Jun 29 2023 at 18:59):

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.

view this post on Zulip Elias Mulhall (Jun 30 2023 at 04:24):

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.

view this post on Zulip Richard Feldman (Nov 25 2024 at 21:32):

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)

view this post on Zulip Kilian Vounckx (Nov 26 2024 at 06:09):

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

view this post on Zulip jan kili (Nov 26 2024 at 06:19):

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.

view this post on Zulip Kilian Vounckx (Nov 26 2024 at 08:57):

JanCVanB said:

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.

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.

view this post on Zulip Kilian Vounckx (Nov 26 2024 at 18:06):

JanCVanB said:

Kilian Vounckx said:

JanCVanB said:

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.

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.

view this post on Zulip Brendan Hansknecht (Nov 26 2024 at 18:35):

Yeah, not supported in current roc, but the plan is to require the module prefix.

someConst = 42
foo = 42
when foo is
    WhateverCurrentModule.someConst -> {}
    _ -> {}

view this post on Zulip jan kili (Nov 26 2024 at 18:36):

Oh wow I thought pattern matching against a variable name worked today. Yes please!

view this post on Zulip Brendan Hansknecht (Nov 26 2024 at 18:36):

It doesn't cause it creates a new variable

view this post on Zulip Brendan Hansknecht (Nov 26 2024 at 18:36):

when foo is
    someConst -> #someConst is a variable with the value of foo.
    _ -> {}

view this post on Zulip jan kili (Nov 26 2024 at 18:38):

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.

view this post on Zulip jan kili (Nov 26 2024 at 18:40):

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)

view this post on Zulip jan kili (Nov 26 2024 at 18:41):

CurrentModule.answer seems verbose.

view this post on Zulip jan kili (Nov 26 2024 at 18:42):

We can move this discussion to another topic, though.

view this post on Zulip jan kili (Nov 26 2024 at 18:46):

Also, "Why not...?" spoken verrrry casually as Not The Person Who Would Implement This.

view this post on Zulip Brendan Hansknecht (Nov 26 2024 at 19:54):

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.

view this post on Zulip Brendan Hansknecht (Nov 26 2024 at 19:58):

I'm gonna move messages, sorry if it is a mess for a minute, they messages are interspersed.

view this post on Zulip Notification Bot (Nov 26 2024 at 19:58):

A message was moved here from #ideas > snake_case instead of camelCase by Brendan Hansknecht.

view this post on Zulip Sky Rose (Nov 27 2024 at 13:27):

Elixir solves this with the ^ pin operator.

view this post on Zulip Sky Rose (Nov 27 2024 at 13:31):

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.

view this post on Zulip Richard Feldman (Nov 27 2024 at 14:24):

of note, you can always do _ if foo == constant -> ...

view this post on Zulip Richard Feldman (Nov 27 2024 at 14:25):

or Ok foo if foo == constant -> ... etc.

view this post on Zulip Richard Feldman (Nov 27 2024 at 14:25):

so allowing module-qualified constants is essentially syntax sugar to save you from writing the if

view this post on Zulip Brendan Hansknecht (Nov 27 2024 at 15:51):

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