7

Is there a reason why I can use a generic function with different type arguments when I pass it as a local value but not when passed as parameter? For example:

let f = id

let g (x,y) = (f x, f y)

g ( 1, '2')

works fine, but if I try to pass the function as parameter

let g f (x,y) = (f x, f y)

g id ( 1, '2')

it fails because it takes the version f < int > and it tries to apply it twice.

I've found a workaround but it forces me to write twice the function I'm passing:

let g f1 f2 (x,y) = (f1 x, f2 y)

g id id ( 1, '2')

This is the second time I face this problem.

Why it behaves this way, it's not supposed to be the same if the function is a local value or if it's passed as parameter?

Is there a way to do this without duplicating the function?

A hack, maybe using explicit type constraints, inline magic, quotations?

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
Michael
  • 73
  • 4

5 Answers5

11

Here's the inline magic. Let's take kvb's code and define a single gmap function that handles all cases:

let inline gmap f (x, y) = f $ x, f $ y

type One = One with static member ($) (One, x) = 1  // Example1 ConvertAll
type Id  = Id  with static member ($) (Id , x) = x  // Example2 PassThrough

type SeqSingleton  = SeqSingleton  with static member ($) (SeqSingleton , x) = seq [x]
type ListSingleton = ListSingleton with static member ($) (ListSingleton, x) = [x]
type ListHead      = ListHead      with static member ($) (ListHead, x) = List.head x

// Usage
let pair1 = gmap One ("test", true)
let pair2 = gmap Id  ("test", true)
let pair3 = gmap SeqSingleton  ("test", true)
let pair4 = gmap ListSingleton ("test", true)
let pair5 = gmap ListHead (["test";"test2"], [true;false])

let pair6 = ("test", true) |> gmap ListSingleton |> gmap ListHead

(* results
val pair1 : int * int = (1, 1)
val pair2 : string * bool = ("test", true)
val pair3 : seq<string> * seq<bool> = (["test"], [true])
val pair4 : string list * bool list = (["test"], [true])
val pair5 : string * bool = ("test", true)
val pair6 : string * bool = ("test", true)
*)

UPDATE

It's also possible to use the even more generic gmap function defined here then it will also work with n-uples (n < 9).

Gus
  • 25,839
  • 2
  • 51
  • 76
  • Hey, finally a very nice workaround. Looks like inline magic is more flexible than higher-rank types :) . I love it. Thanks a lot !!! – Michael Aug 29 '11 at 08:29
7

As rkhayrov mentioned in a comment, type inference is impossible when you can have higher ranked types. In your example, you have

let g f (x,y) = (f x, f y)

Here are two possible types for g which are incompatible (written in a sort of hybrid F#/Haskell syntax):

  1. forall 'b,'c,'d. ((forall 'a . 'a -> 'b) -> 'c * 'd -> 'b * 'b)
  2. forall 'c, 'd. (forall 'a . 'a -> 'a) -> 'c * 'd -> 'c * 'd)

Given the first type, we could call g (fun x -> 1) ("test", true) and get (1,1). Given the second type, we could call g id ("test", true) and get ("test", true). Neither type is more general than the other.

If you want to use higher ranked types in F#, you can, but you have to be explicit and use an intermediate nominal type. Here's one way to encode each of the possibilities above:

module Example1 = 
    type ConvertAll<'b> =
        abstract Invoke<'a> : 'a -> 'b

    let g (f:ConvertAll<'b>) (x,y) = (f.Invoke x, f.Invoke y)

    //usage
    let pair = g { new ConvertAll<int> with member __.Invoke(x) = 1 } ("test", true)

module Example2 = 
    type PassThrough =
        abstract Invoke<'a> : 'a -> 'a

    let g (f:PassThrough) (x,y) = (f.Invoke x, f.Invoke y)

    //usage
    let pair = g { new PassThrough with member __.Invoke(x) = x } ("test", true)
kvb
  • 54,864
  • 2
  • 91
  • 133
  • Thanks for your answer. So I need to write one version of g for each kind of transformation. I think the same applies to higher-rank types in Haskell, right? – Michael Aug 29 '11 at 08:13
  • @Michael - I'm not a Haskell expert, but I believe that's correct. – kvb Aug 29 '11 at 14:41
  • Type-inference is not necessarily impossible when you have higher-ranked types. They are still possible with restrictions (e.g. rank-2, remove recursive types, add annotations). See https://github.com/cdiggins/type-inference for an algorithm for inferring higher-rank types. Or research.microsoft.com/en-us/um/people/simonpj/papers/higher-rank/putting.pdf – cdiggins Jan 03 '18 at 22:25
  • 1
    @cdiggins - sure, that's a good point; however since you lose principle types when moving beyond rank 1, even if a type can be inferred it's not necessarily the "right" one, as the example in my answer shows. – kvb Jan 04 '18 at 14:47
2

I guess this is the expected behaviour:

In the first case you call two different versions of f (one with int, one with char), in the second case you use the same for both and the compiler infers it (top-bottom, left-right - remember?) to be int->int

The problem is that the generic version will be translated in a concrete one by the compiler. I see no workaround for this - not even with inline but perhaps someone can work some magic here ;)

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • To expand: in the second version, when it tries to infer the type of ``f``, it looks at the arguments you give it. You gave it a number, so it inferred it is ``int -> 'b``. But then it sees that you gave it a string, and gets an error because it cannot unify ``int`` and ``string``. – Ramon Snir Aug 27 '11 at 09:32
  • So the compiler always translate a generic version to a concrete one when passing a function as parameter? Is it always so? But I thought generics were supported at IL level. – Michael Aug 27 '11 at 09:54
  • you are right - the generic is IL level but the compiler will check the types - so I tend to find it easier if I just imagine it as *translated* – Random Dev Aug 27 '11 at 11:08
0

I don't think even inline magic will work here, for example trying

 let inline g f (x:^a,y:^b) = (f x,f y);;

in FSI fails as the compiler infers that ^b must be the same as ^a

Thinking about this, the failure becomes a little more obvious, since we take the function f and apply it to x, it must have a signature like ^a -> 'c, which will fail if we try to apply the function to a element with type ^b.

If you think about this problem though, I can't define a function which takes int->string and expect it to work for char->string without overloads, and you can't pass in the overloads as arguments.

John Palmer
  • 25,356
  • 3
  • 48
  • 67
  • Yes, I just mentioned inline, because in the first example I have the function defined as a local value and it works, and I know inline take functions and rewrite them locally. – Michael Aug 27 '11 at 09:57
0

The reason for this is that when f is passed as parameter to g then the compiler will try to infer the type of f. Based on the usage of f in the body of function g, the compiler infers that the function is applied to both x and y that means that both x and y parameters has to be of same type and that is the type of the f parameter. In your code example you pass x as integer and that makes the compiler infer that y will also be of type integer.

UPDATE:

You can do something like:

let g (f:obj -> obj) (x : 'a,y : 'b) = (f x :?> 'a ,f y :?> 'b)
g id (1,"1") |> printfn "%A"

The function f need to make sure it returns the same main type that it receives as obj type

Ankur
  • 33,367
  • 2
  • 46
  • 72
  • Sepcifying types will also not work. Ex: for f you specify 'a -> 'a then x and y both becomes 'a i.e of same type – Ankur Aug 27 '11 at 09:50
  • Can you tell us any 'real code' that your are trying to use? or is it just fighting with the compiler :) – Ankur Aug 27 '11 at 09:53
  • For example now I'm working with key value pairs of generic types, and I want to define a function that will map a function into both key and values, but they can have different types and the function passed is able to deal with those types. So what I'm doing at the moment is passing the function twice as showed on my third example, the code looks really stupid, if you look it the first thing you think is "this could be solved using a function that takes a value replicate it", but it can't. – Michael Aug 27 '11 at 10:20
  • 2
    @Michael You need higher ranked types to write down type signature for this function. E.g. in Haskell it can be written as `g :: (forall a . a -> a) -> (b, c) -> (b, c)`. Unfortunately, they make type inference undecidable, and probably don't play well with CLR, so F# compiler has reasons not to support them. – rkhayrov Aug 27 '11 at 10:26
  • You said that the 'function passed is able to deal with those types' then the parameter to the function should be either a base class or object or some interface that both the types in tuple implements – Ankur Aug 27 '11 at 11:10
  • Updated my answer for a possible solution – Ankur Aug 27 '11 at 11:32
  • Thanks for your answer, it was close but as you said it works only for functions returning the same type. If I try it with another function as for example Seq.singleton it will not work. – Michael Aug 27 '11 at 11:55
  • That would not be possible at all in a static typed language – Ankur Aug 27 '11 at 13:12
  • Actually, you've posed an interesting problem. I've thought you are only interested in `(a -> a)` functions as arguments to `g`, but `Seq.singleton` is another beast. Again, we can write something like `(forall a . a -> [a]) ...` in Haskell, or even `(forall a . a -> b a) -> (c, d) -> (b c, b d)`, but these will stop accepting `id`. Still, I fail to see why this should be impossible in a statically typed language, the question is, what kind of type system we need to encode it. – rkhayrov Aug 27 '11 at 14:23
  • I think calling the function where it is required like : (id a, Seq.singleton b) is the simplest way to do it rather than going too deep – Ankur Aug 27 '11 at 15:45