Warm tip: This article is reproduced from serverfault.com, please click
f#

Is there a way to make F# keep the type of a parameter generic?

发布于 2020-12-02 20:28:51

I'm new to F# (and enjoying it so far), and I've been experimenting with code. I found something that I understand the cause of but not how to overcome it.

I have some code similar to the following:

let pairTest list = if List.length list = 2 then Some list else None

// This seems to compile and work just fine
[ [1; 2] ]
|> List.choose pairTest
|> List.map (fun l -> l |> List.map (fun i -> "a"))
|> List.choose pairTest
|> printfn "%A"

let testFunc groupTest =
  [ [1; 2] ]
  |> List.choose groupTest // Okay
  |> List.map (fun l -> l |> List.map (fun i -> "a"))
  |> List.choose groupTest // Error: expected int list, not string list
  |> printfn "%A"

testFunc pairTest

I understand how F#'s type inference works, and I can see that in testFunc it's hitting int list on the first call to groupTest and then assigning that type (and examining the groupTest paramter in an IDE shows the type as int list -> int list option), which then makes it invalid for its second invocation. However, if I run the code outside of a function (the first function block), it works just fine and seamlessly switches pairTest<'a> between int list and string list.

My question is this: Is there a way to keep F# from locking down the type of groupTest? Or, if not, is there an (F#-idiomatic) way to approach this?

In this specific instance, I can just pass in a length parameter and move the List.length check inside the function, but I'm still curious about the answer for the more general case (if, say, my check were more complex, for instance.)

Questioner
Alphacat
Viewed
0
Tomas Petricek 2020-12-03 05:20:53

This cannot be done in F# with just ordinary functions. The issue is that in F#, you cannot pass a generic function as an argument to another function. When F# prints the type of your testFunc, you get:

('a list -> 'a list option) -> unit

The key thing here is that 'a is a type variable that is fixed when you invoke the function. Formally, there is a universal quantification over the whole type, i.e.:

forall 'a . (('a list -> 'a list option) -> unit)

This means that when you call testFunc, you first set 'a to one specific type. What you need here (and what F# does not support) is to have the quantification only for the type of the argument:

(forall 'a . ('a list -> 'a list option)) -> unit

If you could do this, then the body of the function could, itself, be able to set 'a to two different types when it accesses the function. This cannot be done in F# with plain functions, but you can encode this using an interface:

type GroupTest =
  abstract Invoke<'T> : 'T list -> 'T list option

This interface has a generic method, i.e. essentially a function of type (forall 'a . ('a list -> 'a list option)). You can then write testFunc as a function of GroupTest -> unit:

let testFunc (groupTest:GroupTest) =
  [ [1; 2] ]
  |> List.choose groupTest.Invoke
  |> List.map (fun l -> l |> List.map (fun i -> "a"))
  |> List.choose groupTest.Invoke
  |> printfn "%A"

testFunc { new GroupTest with member x.Invoke a = pairTest a }

The syntax for this is a bit clumsy, so I would only use this trick if it was something essential about my domain. If you need this just in one place, you're probably better off with a simple trick like duplicating the argument (or maybe duplicating the whole testFunc function for each different groupTest). But the interface trick works and it's a general way to do what you're asking about.

let testFunc groupTest1 groupTest2 =
  [ [1; 2] ]
  |> List.choose groupTest1
  |> List.map (fun l -> l |> List.map (fun i -> "a"))
  |> List.choose groupTest2
  |> printfn "%A"

testFunc pairTest pairTest