3

I'm trying to create a function in F# that will convert certain types to a string, but not others. The objective is so that a primitive can be passed but a complex object cannot be passed by accident. Here's what I have so far:

type Conversions =
    static member Convert (value:int) =
        value.ToString()
    static member Convert (value:bool) =
        value.ToString()

let inline convHelper< ^t, ^v when ^t : (static member Convert : ^v -> string) > (value:^v) =
    ( ^t : (static member Convert : ^v -> string) (value))

let inline conv (value:^v) = convHelper<Conversions, ^v>(value)

Unfortunately, my conv function gets the following compile-time error:

A unique overload for method 'Convert' could not be determined based on type information
prior to this program point. A type annotation may be needed. Candidates: 
    static member Conversions.Convert : value:bool -> string, 
    static member Conversions.Convert : value:int -> string

What am I doing wrong?

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
luksan
  • 7,661
  • 3
  • 36
  • 38
  • I think you need a catch-all overload. This trick is typically used for extending built-in functions and operators, and the catch-all provides the default behavior. Which means you probably won't be able to use this to limit an argument to only a few types. – Daniel Feb 03 '14 at 22:41
  • You're right, adding the catch-all overload works, but that kind of defeats the purpose. I wanted the compiler to catch when the wrong argument types were passed. If I have a catch-all overload I have to throw an exception at runtime when the wrong argument types are passed. Other examples I have seen don't have one, like this: http://stackoverflow.com/questions/21194565/null-coalescing-operator-in-f – luksan Feb 03 '14 at 22:46
  • What about using a measure type? – John Palmer Feb 03 '14 at 22:48
  • Don't think a measure type is applicable here - the point was just to clean up some code by not having to explicitly convert values to string but still maintain type-safety. I eventually wanted to add option conversion as well. The funny thing is this seems more elementary than other similar examples I have seen, yet it doesn't work. Basically I'm just trying to work-around the fact that I can't overload a function. – luksan Feb 03 '14 at 22:53

3 Answers3

6

This seems to work:

type Conversions = Conversions with 
    static member ($) (Conversions, value: int) = value.ToString()
    static member ($) (Conversions, value: bool) = value.ToString()

let inline conv value = Conversions $ value

conv 1 |> ignore
conv true |> ignore
conv "foo" |> ignore //won't compile
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • That worked! I added my own answer to further expound on what I was trying to do. However, I am still curious why the original didn't work. It seems that there is something special about operators that allows them to have better type inference while still allowing overloads. Interesting! – luksan Feb 03 '14 at 23:31
  • @luksan Take a look at FsControl ( https://github.com/gmpl/FsControl ) and FSharpPlus ( https://github.com/gmpl/FSharpPlus ) for things like this. In particular, conversions to string like the one you mention here: https://github.com/gmpl/FSharpPlus/blob/master/FSharpPlus/Operators.fs#L156 – Mauricio Scheffer Feb 04 '14 at 02:06
4

For some reason it seems using (^t or ^v) in the constraint instead of just ^v makes it work.

type Conversions =
    static member Convert (value:int) =
        value.ToString()
    static member Convert (value:bool) =
        value.ToString()

let inline convHelper< ^t, ^v when (^t or ^v) : (static member Convert : ^v -> string)> value =
    ( (^t or ^v) : (static member Convert : ^v -> string) (value))

let inline conv value = convHelper<Conversions, _>(value)

Of course it means the function will also compile if the argument's type has a static method Convert from itself to string, but it's highly unlikely to ever bite you.

Tarmil
  • 11,177
  • 30
  • 35
2

Well, Daniel's answer worked. Here's what I wanted in the end:

type Conversions = Conversions with
    static member ($) (c:Conversions, value:#IConvertible) =
        value.ToString()
    static member ($) (c:Conversions, value:#IConvertible option) =
        match value with
        | Some x -> x.ToString()
        | None -> ""

let inline conv value = Conversions $ value

The IConvertible interface type is just a convenient way for me to capture all primitives.

This results in the following behavior (FSI):

conv 1         // Produces "1"
conv (Some 1)  // Produces "1"
conv None      // Produces ""
conv obj()     // Compiler error
luksan
  • 7,661
  • 3
  • 36
  • 38