16

I would like to overload the (/) operator in F# for strings and preserve the meaning for numbers.

/// Combines to path strings
let (/) path1 path2 = Path.Combine(path1,path2)

let x = 3 / 4 // doesn't compile

If I try the following I get "Warning 29 Extension members cannot provide operator overloads. Consider defining the operator as part of the type definition instead."

/// Combines to path strings
type System.String with
  static member (/) (path1,path2) = Path.Combine(path1,path2)

Any ideas?

Regards, forki

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
forki23
  • 2,784
  • 1
  • 28
  • 42

4 Answers4

24

You cannot provide overloaded operators for existing types. One option is to use another operator name (as Natahan suggests). However, you can also define a new type to represent paths in your F# code and provide the / operator for this type:

open System    

// Simple type for representing paths
type Path(p) =
  // Returns the path as a string
  member x.Path = p 
  // Combines two paths
  static member ( / )(p1:Path, p2:Path) = 
    Path(IO.Path.Combine(p1.Path, p2.Path))

let n = 4 / 2
let p = Path("C:\\") / Path("Temp")

This has one important benefit - by making the types more explicit, you give the type checker more information that it can use to verify your code. If you use strings to represent paths, then you can easily confuse path with some other string (e.g. name). If you define your Path type, the type-checker will prevent you from making this mistake.

Moreover, the compiler won't allow you to (simply) combine paths incorrectly (which can easily happen if you represent paths as strings), because p + p is not defined (you can use only /, which correctly uses Path.Combine).

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
10

I don't think that there is a straightforward way to do that. Extension members aren't taken into consideration for operator overloading in F#, and there isn't a good way to redefine the operation in a semi-generic way using member constraints.

It is possible to hack something together that will work, but it's very ugly:

type DivisionOperations =
  static member Divide(x:int, y:int) = x / y
  static member Divide(path1, path2) = Path.Combine(path1, path2)

let inline div< ^t, ^a, ^b, ^c when (^t or ^a) : (static member Divide : ^a * ^b -> ^c)> a b = ((^t or ^a) : (static member Divide : ^a * ^b -> ^c) (a, b))

let inline (/) x y = div<DivisionOperations, _, _, _> x y
kvb
  • 54,864
  • 2
  • 91
  • 133
10

Actually you can.

Try this:

open System.IO

type DivExtension = DivExtension with
    static member inline (=>) (x             , DivExtension) = fun y -> x / y
    static member        (=>) (x             , DivExtension) = fun y -> Path.Combine(x, y)
    static member        (=>) (x:DivExtension, DivExtension) = fun DivExtension -> x

let inline (/) x y = (x => DivExtension) y
Gus
  • 25,839
  • 2
  • 51
  • 76
  • 2
    Note that this is actually quite similar to my answer, although by adding the dummy `DivExtension * DivExtesion -> DivExtension -> DivExtension` overload you have been able to use generalize the first overload while preventing the compiler from defaulting the `(/)` operator to the second overload, which is very clever. I feel that your use of the `(=>)` symbolic operator makes the body of `(/)` harder to understand, although you do get to omit the explicit static member constraints. – kvb Dec 13 '11 at 15:32
  • 1
    Yes, you're right. I prefer to use intermediate operators to avoid writing the static constraints. In your answer you can also add the dummy overload and it will generalize as well. One thing I can't do without operators is to write twice the 'or', I mean something like when (^t or ^a or ^b). In those cases I use the ternary operator, I think it's the only way. – Gus Dec 13 '11 at 20:08
  • Hi, I tested and it works, but i really don't understand why ( I am new to F#)... – Liviu Feb 17 '14 at 13:47
  • @Liviu It's based on overloading. This technique is explained here http://www.nut-cracker.com.ar – Gus Feb 17 '14 at 14:21
  • 1
    http://www.nut-cracker.com.ar/ is not responding, it seems to have moved to: http://nut-cracker.azurewebsites.net/ – JJJ Mar 06 '15 at 08:02
  • @JJJ Yes, it's moved there. Thanks. – Gus Mar 06 '15 at 08:04
  • @Gustavo I'm trying to understand the null coalecing operator defined here: http://stackoverflow.com/a/21194566/5547, which links to this question. Your way of defining things looks simpler, except that I can't find anything about the (=>) operator, do you have a link to it's definition? – JJJ Mar 06 '15 at 08:15
  • @Gustavo nevermind, it's me being stupid, you are ofcourse defining an operator. Sorry – JJJ Mar 06 '15 at 09:18
  • @JJJ yes, it's just an arbitrary operator. Might be confusing at first sight. – Gus Mar 06 '15 at 09:42
  • @Gustavo: This is very cool, but the third static member seems like black magic to me. It seems to reduce to the identity function (`id`), which is effectively a no-op. Why is it needed? Thanks. – Brian Berns Aug 22 '18 at 17:42
  • 1
    @brianberns It is there to create the necessary ambiguity between the overloads. Otherwise it will try to resolve them in advance (try removing it). – Gus Aug 23 '18 at 06:23
  • @Gustavo Thanks. I see what you mean when I remove it. However, I still don't understand why a third member is required to create ambiguity, and why that particular member signature causes the `(/)` operator to generalize as desired. – Brian Berns Aug 23 '18 at 15:27
  • 1
    Yes, the overload resolution algorithm is more complicated that most people think. You can have a look at the F# spec, but I think the F# compiler is not 100% compliant with that spec. Basically when a method wins it resolve to that one. Creating another method with a convenient signature makes tie-breaker algorithm unable to decide, and that's what we want in this case. – Gus Aug 23 '18 at 16:20
  • @Gustavo Ah, I see now. I thought there was something special about that particular signature, but it looks like any third member will create the necessary ambiguity. For example, this signature also works: `static member (=>) (x : char, DivExtension)` – Brian Berns Aug 23 '18 at 18:08
6

I do not think this is possible in F#, based on a reading of the overloading documentation.

I would instead suggest that you create your own function which looks like / but isn't. Something like:

let (</>) path1 path2 = Path.Combine (path1,path2)

This is likely to be less annoying in the long run because it doesn't mess with the implicit type inference that the human reader is running--/ means that the result is a floating point, and remembering that it's sometimes a string is a burden*. But after the first time the reader sees </>, it's easy to remember that it does something related to the symbol embedded in the middle.

*I think the only reason + for strings looks OK is over-exposure. After using Haskell or Caml for a long time, the first few minutes after switching to another language makes "foo" + "bar" look jarringly bad.

Nathan Shively-Sanders
  • 18,329
  • 4
  • 46
  • 56