fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Constant expressions in patterns

Open IS4Code opened this issue 10 months ago • 9 comments

I propose we have a way to embed arbitrary constant expressions in patterns, such as via the following syntax:

match x
| 1 -> () // a single literal, already allowed
| const (1 + 10 * 20) -> () // a constant expression
| const X -> () // defined literal identifier, not a variable

The const pattern has the same semantics as a simple constant pattern. The precedence should be the same as for const used for static arguments, i.e. to require parentheses for complex expressions.

The existing way of approaching this problem in F# is to define a literal value with the desired value (which must be placed in a module however, until #848 is resolved), or an active pattern that does the comparison (which is inefficient). In either case there is no way to in-line the expression.

Pros and Cons

The advantages of making this adjustment to F# are simplifying code that needs to match on constant values derived from multiple sources, such as "[" + nameof X + "]", or has embedded meaning, such as 1000<m> / 20<m/s>. Not all such constants are "magic values" to warrant giving them a name, for example flags enums:

open System.Reflection
open type TypeAttributes

let t = typeof<int>
let flags = t.Attributes &&& (Abstract ||| Sealed ||| Interface)

match flags with
| Abstract -> printf "abstract class" // incorrect - interpreted as a variable name even with the `open type`
| Abstract ||| Sealed -> printf "static class" // incorrect - unexpected symbol
| Sealed -> printf "sealed class"
| Class -> printf "class"
| _ -> printf "other"

With a way to use an arbitrary expression, such a code could be written without significant restructuring as:

match flags with
| const Abstract -> printf "abstract class"
| const (Abstract ||| Sealed) -> printf "static class"
| const Sealed -> printf "sealed class"
| const Class -> printf "class"
| _ -> printf "other"

The disadvantages of making this adjustment to F# are permitting a way to produce potentially "less clean" code. However, not having this feature also leads to significantly unclean code in some situations.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions: #1018

Affidavit (please submit!)

  • [x] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue

  • [x] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository

  • [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it

  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate

  • [x] This is not a breaking change to the F# language design

  • [x] I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

IS4Code avatar Feb 14 '25 12:02 IS4Code

Related: https://github.com/fsharp/fslang-suggestions/issues/1018.

brianrourkeboll avatar Feb 14 '25 13:02 brianrourkeboll

Not a fan of a whole new language construct to be honest. If we really want to enable arbitrary constant expressions in the matching, we may instead think of a way of reducing/eliminating confusion and ambiguity (for both compiler and user) in cases of constants vs bindings of variables in cases instead.

We should also keep in mind how will this affect pattern compilations and exhaustiveness checks, performance wise.

vzarytovskii avatar Feb 17 '25 09:02 vzarytovskii

I would also look at the enums without qualified enum name separately, this could be addressed separately without bringing in broader support for complex constant expressions.

T-Gro avatar Feb 17 '25 09:02 T-Gro

I would also look at the enums without qualified enum name separately, this could be addressed separately without bringing in broader support for complex constant expressions.

+1, this issue has been brought up multiple times already

vzarytovskii avatar Feb 17 '25 10:02 vzarytovskii

I would rather prefer having a general way that automatically covers enum cases too, i.e. const Name instead of, say, enum Name.

That being said, there are other options too ‒ if type-qualified lookup of members in patterns is extended instead, I could (once dotnet/fsharp#18316 is fixed) write Const<const (Abstract ||| Sealed)>.Value and I will be happy too. If that is more viable, I can make a separate suggestion for that.

IS4Code avatar Feb 17 '25 11:02 IS4Code

I feel like parameterized active patterns might already work for this?

voronoipotato avatar Feb 25 '25 17:02 voronoipotato

@voronoipotato I felt the same way, but it does not seem to be the case. The spec has separate grammar rules for patterns which do not cover arbitrary expressions, even if they happen to become an active pattern argument (in that case the rule is to transform the "pattern" into a syntactically equivalent expression).

IS4Code avatar Feb 27 '25 18:02 IS4Code

Thanks for following back up! Yeah I tried and wasn't able to, kinda always felt like I was 6 inches away from an answer

voronoipotato avatar Feb 27 '25 19:02 voronoipotato

I am still not sold on this being an improvement, especially the need to put const in front of it:

match flags with
| const Abstract -> printf "abstract class"
| const (Abstract ||| Sealed) -> printf "static class"
| const Sealed -> printf "sealed class"
| const Class -> printf "class"
| _ -> printf "other"

T-Gro avatar Apr 28 '25 12:04 T-Gro