Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't non-partial active patterns be parameterized in F#?

The following F# code works as I expected, printing `Matched as 'A':

let (|Char|_|) convf = function
    | LazyList.Nil -> None
    | LazyList.Cons (x, _) -> Some (convf x)

let test = function
    | Char System.Char.ToUpper x -> printfn "Matched as %A" x
    | _ -> printfn "Didn't match"

test (LazyList.of_list ['a'])

However, if I change Char from a partial active pattern to a complete active pattern as follows:

let (|Char|NoChar|) convf = function
    | LazyList.Nil -> NoChar
    | LazyList.Cons (x, _) -> Char x

let test = function
    | Char System.Char.ToUpper x -> printfn "Matched as %A" x
    | NoChar System.Char.ToUpper -> printfn "Didn't match"

test (LazyList.of_list ['a'])

Then the code fails to compile, giving the following error message: error FS0191: Only active patterns returning exactly one result may accept arguments.

This example may look somewhat contrived, but it's a simplified version of an active pattern I tried to use in a Prolog lexer I've been working on in my spare time. I can easily rewrite my code to avoid this problem, but I'm curious about why this sort of code is disallowed.

Update: the newer versions of F# seem to have renamed this error:

error FS0722: Only active patterns returning exactly one result may accept arguments

like image 954
bcat Avatar asked Aug 28 '09 23:08

bcat


2 Answers

NB. This is exactly what Brian said, but hopefully stated in a clearer fashion.

I recall logging a bug on precisely this issue and IIRC this is what Don Syme had to say on the matter.

A multi-case active pattern is a conversion function from some input value into one of several output values. In your example, any character is converted to the Char case or the NoChar case.

The benefit of this is that the F# compiler calls the multi-case active pattern function once and can then generally determine which pattern match rule to evaluate next.

If you allow a parameter however, then you need to evaluate the multi-case active pattern for every pattern match rule.

So imagine the following

match input with
| Alpha "foo" -> ...
| Bravo "bar" -> ...

When evaluating (|Alpha|Bravo|) "foo" returned 'Bravo', then the first rule wouldn't match. Likeways (|Alpha|Bravo|) "bar" returns 'Alpha', then the second rule wouldn't match either. So you don't really have a multi-case active pattern. Just a paramterized, partial active pattern. (Because for some inputs the expected pattern-case won't be hit.)

So when confronted with a corner of the language that doesn't make a whole lot of sense, and in fact can be made much clearer by the partial, parameterized active pattern. The feature wasn't added to the language.

like image 129
Chris Smith Avatar answered Oct 17 '22 08:10

Chris Smith


I can't say for certain (don't know actual design rationale), but trying to reverse-engineer it, what would you expect this code to do?

let (|Char|NoChar|) pred = function    
    | LazyList.Nil -> NoChar    
    | LazyList.Cons (x, _) -> if pred x then Char x else NoChar
let test = function    
    | Char System.Char.IsLetter x -> printfn "Matched as %A" x    
    | NoChar System.Char.IsDigit -> printfn "Didn't match"
test (LazyList.of_list ['a'])
test (LazyList.of_list ['1'])

Given that non-partial active patterns are supposed to partition the whole space, it would be weird if you gave each a different argument inside the same match, because then they might 'both fail' or 'both succeed'. (It's also suggestive of how they may implemented, e.g. just as patterns that capture their argument before doing the match. The captured argument would be invariant across all branches of the match.)

It also suggests that you could write e.g.

let test convf l = 
    let (|Char|NoChar|) = function    
        | LazyList.Nil -> NoChar    
        | LazyList.Cons (x, _) -> Char(convf x)
    match l with
    | Char x -> printfn "Matched as %A" x    
    | NoChar -> printfn "Didn't match"
test System.Char.ToUpper (LazyList.of_list ['a'])

(though I dunno if this is convenient/realistic for your particular app).

like image 22
Brian Avatar answered Oct 17 '22 08:10

Brian