Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

anonymous active patterns #1186

Closed
4 of 5 tasks
xp44mm opened this issue Sep 10, 2022 · 12 comments
Closed
4 of 5 tasks

anonymous active patterns #1186

xp44mm opened this issue Sep 10, 2022 · 12 comments

Comments

@xp44mm
Copy link

xp44mm commented Sep 10, 2022

I propose Let's add a syntax for active patterns. We expanded the banana clips (||) from active patterns definition to the pattern match expression.

match input:'t with
| (|function_expr:'t->'v|) (result:'v) -> ..

single case

The existing way of approaching this problem in F# is ...

let coalesce (onNone:'t) (value:'t option) : 't=
    match value with
    | None -> onNone
    | Some e -> e

let (|Default|) onNone value =
    coalesce onNone value

let greet (Default "random citizen" name) =
    printf "Hello, %s!" name

this example is come from https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns

I propose we ...

let greet ((|coalesce "random citizen"|) name) =
    printf "Hello, %s!" name

multi-case active patterns

The existing way of approaching this problem in F# is ...

let (|Even|Odd|) (input:int) = 
    if input % 2 = 0 then Even(input/2) else Odd
let test (input:int) =
    match input with
    | Even x -> sprintf "%d" x
    | Odd -> ""

I propose we ...

type Num = | Even of int | Odd
let tonum (input:int):Num = 
    if input % 2 = 0 then Even(input/2) else Odd

let test(input:int) =
    match input with (|tonum|) m ->
    match m with
    | Even x -> sprintf "%d" x
    | Odd  -> ""

partial active patterns

The existing way of approaching this problem in F# is ...

let tryFactor (n:int) (inp:int):int option = 
    if inp % n = 0 then 
        Some(inp / n) 
    else None
    
let (|Multi|_|) n inp = tryFactor n inp
    
let test(input:int) =
    match input with
    | Multi 2 x -> sprintf "%d" x
    | _ -> sprintf "%d" input

I propose we ...

let test(input:int) =
    match input with
    | (|tryFac 2|) ?(x) -> sprintf "%d" x
    | _ -> sprintf "%d" input

the result of partial active patterns is Some(x), so ?(x) indicates that the active pattern is partial active pattern.

Pros and Cons

The advantages of making this adjustment to F# are ...

The current active patterns is masquerades as DU. Explicitly active patterns make it different from the DU. The code reader knows that pattern matching is using a function to transform from input data to another style.

A banana clip in pattern matching can contain a function expressions, which are more flexible.

The disadvantages of making this adjustment to F# are ...

The Explicitly active patterns is longer than the current active patterns.

Extra information

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

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • 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.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • 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 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@charlesroddie
Copy link

Could you add type annotations to your function arguments? I find it hard to read the code and so understand this proposal without them. This applies for F# code in general, but while you can put code without type arguments into an IDE and see everything, you can't do that with code which depends on features that don't exist yet.

@abelbraaksma
Copy link
Member

abelbraaksma commented Sep 11, 2022

We move the banana clips (||) of active patterns into the pattern match expression.

Why would you ever want to do that? You would take one of the most simple and elegant syntaxes of F#, pattern matching, and turn it into something that's very hard to read.

You even gave the example:

    match input with
    | Even x -> sprintf "%d" x
    | Odd -> ""

vs

    match input with
    | (|tonum|) (Even x) -> sprintf "%A" x
    | (|tonum|) Odd  -> ""

Even though in your example, tonum doesn't match any defined active pattern, so the code you present seems to be broken, even with the new syntax, I think you mean:

    match input with
    | (|Even|Odd|) (Even x) -> sprintf "%A" x
    | (|Even|Odd|) Odd  -> ""

I like proposals that make F# easier to use, or more powerful, but this does neither. Also, it violates the checkboxes (which I assume you didn't check for a reason):

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.

That said, active patterns are just functions, so you can get most of what you want by just using this (hard-to-read) code directly:

let (|Even|Odd|) (input:int) = 
    if input % 2 = 0 then Even(input/2) else Odd

// this is already legal syntax
let x = (|Even|Odd|) 24

Or even something weird like this (nobody should write this):

let (|Even|Odd|) (input:int) = 
    if input % 2 = 0 then Even(input/2) else Odd

let f (|Even|Odd|) a = (|Even|Odd|) a

If you really want to use bananaclips in your match statements, you can get quite close by doing this (just make them return boolean):

let (|Even|) (input:int) = 
    input % 2 = 0
let (|Odd|) (input:int) = 
    input % 2 = 1

let f a = 
    match a with
    | _ when (|Even|) a -> "even"
    | _ when (|Odd|) a -> "odd"

// or directly
let isOdd a = (|Odd|) a

But the code above will be more readable without the bananaclips:

let (|Even|_|) (input:int) = 
    if input % 2 = 0 then Some () else None
let (|Odd|_|) (input:int) = 
    if input % 2 = 1 then Some () else None

let f a = 
    match a with
    | Even -> "even"
    | Odd -> "odd"

Though it still allows you to use bananaclip syntax:

let x = (|Even|_|) 42

PS: anybody else reading this: I don't recommend any of the above coding styles. Bananaclips should only ever be used in the definition of active patterns, it'll greatly confuse readers of your code if you'd start using these directly as functions.

@xp44mm xp44mm changed the title Active Pattern Syntax Explicitly active Pattern Syntax Sep 11, 2022
@xp44mm
Copy link
Author

xp44mm commented Sep 11, 2022

this code:

    match a with
    | _ when (|Even|) a -> "even"
    | _ when (|Odd|) a -> "odd"

It cannot be composed, normal active patterns can be composed.

match (a,b) with
| Even aa, Even bb -> ..
| _ -> ..

The code to convert to the proposed is:

match (a,b) with
| (|tonum|) (Even aa), (|tonum|)(Even bb) -> ..
| _ -> ..

Although longer, a banana clip can contain a function expressions, which are more flexible.

If you're not used to it, it can be understood this way, which move the function expression inside the banana clip precede the input, and the result remains in the pattern. but they're not equivalent.

    match input with
    | Multi 2 x -> ()

<=>
    match input with
    | (|tryFac 2|) ?(x) -> ()

<=!=>
    match tryFac 2 input with
    | Some(x) -> ()

@abelbraaksma
Copy link
Member

abelbraaksma commented Sep 11, 2022

So you’d go from very simple, clean syntax to wordier, more complex hard-to-read syntax for the sole purpose of seeing bananaclips inside match patterns? I’m sorry, but I don’t see any reason why you would complicate syntax and have zero benefit (it’s just another way of doing X), or have added features by doing so.

Also, I was showing how the bananaclip syntax leads to callable functions. If you do want make your code hard-to-read, you can already do so, as I showed, but in general, you shouldn’t.

——————

I’ve noticed you made a bunch of suggestions recently and that’s great, we welcome community ideas. However, before you make a suggestion it’s worthwhile first asking your question on StackOverflow or F# Slack. People can then point you to ways to use current features to solve your coding problems, or point you to existing suggestions that are out there already.

Good language suggestions don’t go against F#’s language philosophy, instead they simplify coding or make things possible that weren’t before (like interop, byref, native, UoM on other types etc) and don’t add a new way of doing things that are already possible.

These are just my friendly suggestions to you, to help you make future good proposals for F#. I’ve been there were others didn’t like my language suggestions simply because I didn’t do enough research before and asking the community online if people are interested (F# Slack is great for that).

@abelbraaksma
Copy link
Member

abelbraaksma commented Sep 25, 2022

Although longer, a banana clip can contain a function expressions, which are more flexible.

I missed this line in your comment. You can do this already, in fact, you can have multiple arguments to your active pattern match expressions. You have all the flexibility you’d need.

@cartermp
Copy link
Member

cartermp commented Oct 5, 2022

I would propose this is reworded as "anonymous active patterns", since it seems to be mostly about being able to define them as ad-hoc and anonymous. I think it's an interesting idea in principle, but I've not really come across many situations where I use active patterns a lot and would like them to be defined ad-hoc and anonymous. It's usually the case that the pattern abstracts over a more complicated function call, and having the nicer name and ability to use match makes it feel simplified.

@xp44mm
Copy link
Author

xp44mm commented Oct 6, 2022

@cartermp

Sometimes, A simple function expression are more readable than active pattern case name:

    match input with
    | Even -> sprintf "%d" input
    | Odd -> ""

vs

let flip f x y =  f y x

match input with
| (|flip (%) 2|) 0 -> sprintf "%d" input
| _ -> ""

@xp44mm xp44mm changed the title Explicitly active Pattern Syntax anonymous active patterns Oct 11, 2022
@abelbraaksma
Copy link
Member

abelbraaksma commented Oct 22, 2022

While I disagree with this being "more readable", at least not the example above, let's at least investigate what we already have. As it turns out, you can use anonymous functions, curried banana-clip functions and operators-turned-functions in pattern syntax.

Combine all this, and you can get pretty close to "anonymous active patterns", or "ad-hoc patterns". Just write a pattern that takes a function as an argument. While pattern syntax forbids fun and function keywords, and also infix operators, you can get quite far, and at the very least, you can write the flip argument (almost) like above.

module Patterns =
    let (|Anon|_|) f x =
        if f x then Some ()
        else None

    let (|Banana|_|) f x =
        match f x with
        | Some _ -> Some()
        | None -> None


    let tryMe x =
        // invert arguments
        let inv f a b = f b a
        let chain f g = f >> g
        let checkEven = fun x -> x % 2 = 0

        // ad-hoc active pattern
        let (|FortyTwo|) x = if x = 42 then Some x else None

        match x with
        | Banana (|FortyTwo|) -> "FortyTwo!" // banana-clip function as argument 
        | Anon ((=) 2) -> "It's TWO!"  // single anonymous function, "equal to"
        | Anon ((>) 10) -> "It's smaller than 10!" // other anonymous function, "smaller than"
        | Anon (chain (inv (%) 2) ((=) 1)) -> "Odd"  // bunch of functions anonymously combined
        | Anon ((>>) (inv (%) 2) ((=) 1)) -> "Odd"  // alternative syntax, without "chain"
        | Anon checkEven -> "Any other even number"  // any local function
        | _ -> "Impossible"

@abelbraaksma
Copy link
Member

abelbraaksma commented Oct 22, 2022

And in case you'd wonder whether the above jiggery pokery works with flip from your example, you just need one argument extra. Let's call it Eq, because you are checking for equality after the (%) 2 is executed:

module Patterns =
    let (|Eq|_|) f y x =
        if f x = y then Some ()
        else None

    let tryMe x =
        // flipper
        let flip f x y =  f y x

        match x with
        | Eq (flip (%) 3) 0 -> "Divisible by three"
        | Eq (flip (%) 2) 1 -> "Uneven"
        | Eq (flip (%) 2) 0 -> "Even"

So, basically, what you're after, use of small functions in your active patterns, "just works":

> tryMe 5;;
val it: string = "Uneven"

> tryMe 6;;
val it: string = "Divisible by three"

> tryMe 2;;
val it: string = "Even"

And I'd argue it's more flexible than potentially "anonymous active patterns" can be, as it doesn't limit the result type, while active patterns require the result to be Some|None, which typically leads to larger functions to write inline.

PS, this is probably clearer, still, but that's up to you, any syntax can be used:

let tryMe x =
    let modulo a b = b % a

    match x with
    | Eq (modulo 3) 0 -> "Divisible by three"
    | Eq (modulo 2) 1 -> "Uneven"
    | Eq (modulo 2) 0 -> "Even"

From your earlier comments:

It cannot be composed, normal active patterns can be composed.

These can, this is fine:

match x, y with
| Eq (modulo 3) 0, Eq (modulo 3) 0 -> "Both divisible by three"

Although longer, a banana clip can contain a function expressions, which are more flexible.

As can active patterns, they can even take an active pattern as argument, as shown above. You, as an author, decides how flexible you allow them to be.

@xp44mm
Copy link
Author

xp44mm commented Oct 23, 2022

Active Patterns and DUs are likely to be confused, and their names are likely to conflict. When a programmer sees EQ, he needs to browse the context code to determine whether the name is AP or DU.

AP and DU are originally two independent notions. We mix them together into a uniform, (DU case Name convention), and then use the AP parameters to separate AP from DU. They should not have been mixed together in the first place.

@abelbraaksma
Copy link
Member

They should not have been mixed together in the first place.

They should. It’s an implementation detail. The user should not concern him/her/they with implementation details. And where it matters, ie at construction, they cannot be confused.

I was merely giving you an example that what you’re proposing is possible. And as shown, you can even use banana clips inside a match expression, as you so clearly consider a good thing. If you don’t like the Eq name, that’s fine, you can name it whatever you like to make it better stand out.

I like your principled idea of more expressive freedom in patterns. You were clearly onto something. Before your post I didn’t know this syntax was already allowed. But now that we know it is, I don’t think it’s worth adding it, since, well, it’s already possible and it falls into the category of “more ways to do the same thing”.

But I’ve been wrong before and maybe I’m missing an obvious use case that hasn’t yet been shown here.

@dsyme
Copy link
Collaborator

dsyme commented Oct 28, 2022

I get the idea - that you could turn any function into an active pattern by putting it's name in banana-clips.

However I agree with @abelbraaksma's that it doesn't meet a general readability/utility bar - and in many ways is something we already decided against in the design of APs and conflicts with the design choices already made

@dsyme dsyme closed this as completed Oct 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants