cocogorilla
cocogorilla

Reputation: 1865

f# signature matching explained

I am running into difficulty with F# in numerous scenarios. I believe I'm not grasping some fundamental concepts. I'm hoping someone can track my reasoning and figure out the (probably many) things I'm missing.

Say I'm using Xunit. What I'd like to do is, provided two lists, apply the Assert.Equal method pairwise. For instance:

Open Xunit
let test1 = [1;2;3]
let test2 = [1;2;4]
List.map2 Assert.Equal test1 test2

The compiler complains that the function Equal does not take one parameter. As far as I can tell, shouldn't map2 be providing it 2 parameters?

As a sanity check, I use the following code in f# immediate:

let doequal = fun x y -> printf "result: %b\n" (x = y)
let test1 = [1;2;3]
let test2 = [1;2;4]
List.map2 doequal test1 test2;;

This seems identical. doequal is a lambda taking two generic parameters and returning unit. List.map2 hands each argument pairwise into the lambda and I get exactly what I expected as output:

result: true
result: true
result: false

So what gives? Source shows Xunit.Equal has signature public static void Equal<T>(T expected, T actual). Why won't my parameters map right over the method signature?

EDIT ONE I thought two variables x and y vs a tuple (x, y) could construct and deconstruct interchangeably. So I tried two options and got different results. It seems the second may be further along than the first.

List.map2 Assert.Equal(test1, test2) The compiler now complains that 'Successive arguments should be separated spaces or tupled'

List.map2(Assert.Equal(test1, test2)) The compiler now complains that 'A unique overload method could not be determined... A type annotation may be needed'

Upvotes: 4

Views: 291

Answers (4)

scrwtp
scrwtp

Reputation: 13577

It's all there in the type signatures.

The signature for Assert.Equals is something along the lines of 'a * 'a -> unit. List.map2 expects a 'a -> 'b -> 'c.

They just don't fit together.

List.map2 (fun x y -> Assert.Equal(x,y)) test1 test2 - works because the lambda wrapping Equals has the expected signature.

List.zip test1 test2 |> List.map Assert.Equal - works because you now have a single list of tuples, and since List.map wants an 'a -> 'b function (where 'a is now a tuple), Assert.Equal is now fair game.

It's simply not true that two values and a tuple are implicitly interchangeable. At least not as far as F# the language is concerned, or the underlying IL representation is concerned. You can think that it's that way when you call into F# code from, say, C# - an 'a -> 'b -> 'c function there is indeed called the same way syntactically as an 'a * 'b -> 'c function - but this is more of an exception than a rule.

Upvotes: 2

Petr
Petr

Reputation: 4280

According to its signature Xunit.Assert.Equal() takes a single 2 values tuple parameter

Upvotes: 1

Grundoon
Grundoon

Reputation: 2764

I think that part of the problem comes from mixing methods (OO style) and functions (FP style).

  • FP style functions have multiple parameters separated by spaces.
  • OO style methods have parens and parameters separated by commas.
  • Methods in other .NET libraries are always called using "tuple" syntax (actually subtly different from tuples though) and a tuple is considered to be one parameter.

The F# compiler tries to handle both approaches, but needs some help occasionally.

One approach is to "wrap" the OO method with an FP function.

// wrap method call with function
let assertEqual x y = Assert.Equal(x,y)

// all FP-style functions
List.map2 assertEqual test1 test2

If you don't create a helper function, you will often need to convert multiple function parameters to one tuple when calling a method "inline" with a lambda:

List.map2 (fun x y -> Assert.Equal(x,y)) test1 test2

When you mix methods and functions in one line, you often get the "Successive arguments should be separated" error.

printfn "%s" "hello".ToUpper()  
// Error: Successive arguments should be separated 
// by spaces or tupled

That's telling you that the compiler is having problems and needs some help!

You can solve this with extra parens around the method call:

printfn "%s" ("hello".ToUpper())  // ok

Or sometimes, with a reverse pipe:

printfn "%s" <| "hello".ToUpper() // ok

The wrapping approach is often worth doing anyway so that you can swap the parameters to make it more suitable for partial application:

// wrap method call with function AND swap params
let contains searchFor (s:string) = s.Contains(searchFor)

// all FP-style functions
["a"; "b"; "c"]
|> List.filter (contains "a")

Note that in the last line I had to use parens to give precedence to contains "a" over List.filter

Upvotes: 7

Mark Seemann
Mark Seemann

Reputation: 233150

public static void Equal<T>(T expected, T actual)

doesn't take two parameters - it takes one parameter, which is a tuple with two elements: (T expected, T actual).

Try this instead:

List.map2 Assert.Equal(test1, test2)

Upvotes: 5

Related Questions