tags:

views:

226

answers:

3

I am currently implementing a Spec framework in F sharp and I want to hide the Equals, GetHashCode etc. methods on my should type, so that the API is not cluttered with these.

I know in C# it is done by making the class implement an interface like this:

using System;
using System.ComponentModel;

public interface IFluentInterface
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    bool Equals(object other);

    [EditorBrowsable(EditorBrowsableState.Never)]
    string ToString();

    [EditorBrowsable(EditorBrowsableState.Never)]
    int GetHashCode();

    [EditorBrowsable(EditorBrowsableState.Never)]
    Type GetType();
}

I tried doing the same in F sharp:

type IFluentInterface = interface

    [<EditorBrowsable(EditorBrowsableState.Never)>]
    abstract Equals : (obj) -> bool

    [<EditorBrowsable(EditorBrowsableState.Never)>]
    abstract ToString: unit -> string

    [<EditorBrowsable(EditorBrowsableState.Never)>]
    abstract GetHashCode: unit -> int

    [<EditorBrowsable(EditorBrowsableState.Never)>]
    abstract GetType : unit -> Type 
end

Implemented it in my type:

        interface IFluentInterface with
        member x.Equals(other) = x.Equals(other)
        member x.ToString()    = x.ToString() 
        member x.GetHashCode() = x.GetHashCode()
        member x.GetType()     = x.GetType() 

but without success.

I also tried to override the methods in my type and adding the attribute that way, but that didn't do the trick either.

So the question remains, how can I clean up my API ?

Edit:

Thanks to the help (see below) I was able to solve my problem.

In summary, .Equals and .GetHashCode can be hidden via [<NoEquality>] [<NoComparison>] but that will also change the semantics.

The hiding via EditorBrowsable attributes does not work.

The only way to have a clean API and still be able to overload methods is to make these method members static.

The resulting class can be found by browsing inside my project FSharpSpec.

The type in question can be found here.

Thanks to everyone who helped me solve this problem.

Cheers ...

+3  A: 

I don't think that there is any way to do that in F# in general. In the particular case of .Equals and .GetHashCode, you can make them unusable by putting a [<NoEquality>] attribute on your type, but this actually has a semantic effect in addition to hiding those methods.

EDIT

It might also be worth mentioning that fluent interfaces are rarely used in F#, since it's much more idiomatic to use combinators and pipelining instead. For instance, imagine that we want to create a way to create arithmetic expressions. Rather than

let x = Expressions.MakeExpr(1).Add(2).Mul(3).Add(4)

I think that most F# users would prefer to write

open Expressions
let x = 
  1
  |> makeExpr
  |> add 2
  |> mul 3
  |> add 4

With this style, there's no need to hide members because expressions get piped to combinators, rather than calling methods of an expression builder.

kvb
Thanks, I also had to put the [<NoComparison>] attribute, which I did. The affected semantics don't matter in this case.Still need to hide ToString() and GetType(), but it looks much cleaner already.
Thorsten Lorenz
@ "fluent interfaces are rarely used in F#"I'm sorry, the name might be misleading, my sole purpose for this interface is to hide above mentioned methods.I just happen to use this kind of interface a lot for creating fluent interfaces in C#.
Thorsten Lorenz
+3  A: 

Alternatively, you could design the library using an alternative style using functions enclosed in a module. This is the usual way for writing functional code in F# and then you won't need to hide any standard .NET methods. To complete the example given by 'kvb', here is an example of object-oriented solution:

type MyNum(n:int) =
  member x.Add(m) = MyNum(n+m)
  member x.Mul(m) = MyNum(n*m)

let n = new MyNum(1)
n.Add(2).Mul(10) // 'ToString' shows in the IntelliSense

The functional way of writing the code might look like this:

type Num = Num of int
module MyNum =
  let create n = Num n
  let add m (Num n) = Num (m + n)
  let mul m (Num n) = Num (m * n)

MyNum.create 1 |> MyNum.add 2 |> MyNum.mul 10

If you type MyNum., the F# IntelliSense will show the functions defined in the module, so you won't see any noise in this case.

Tomas Petricek
I'll look into that, I assume this approach will also allow me to overload the methods? -- On another note, I enjoyed your screen casts on F#.
Thorsten Lorenz
Thanks! Unfortunately, functions declared using `let` cannot be overloaded. Overloading is supported only for `member` declarations. See other SO questions on overloading in F# - e.g. http://stackoverflow.com/questions/2260939/f-overloading-functions
Tomas Petricek
Well in that case I'd need to use a type, since I heavily depend on the ability to overload methods, you can imagine that a should.contain(actual, expected) needs to be implemented differently for strings than for sequences for example.Is it possible to overload static members though? Obviously, if I would make my Assertion class and its members static, it wouldn't show any .ToString() ... methods.
Thorsten Lorenz
Yes, overloading should work for static members.
Tomas Petricek
Thanks, I changed them all to static and now got my cleaned up API (see edit of my question).
Thorsten Lorenz
+4  A: 

Repeating my answer from

http://cs.hubfs.net/forums/thread/13317.aspx

In F# you can disallow Equals & GetHashCode (and remove them from intellisense) by annotating the type with the NoEquality and NoComparison attributes, as shown below. User-defined methods can also be hidden from the intellisense list via the Obsolete attribute or the CompilerMessage attribute with IsHidden=true. There is no way to hide the System.Object methods GetType and ToString from the F# intellisense.

[<NoEquality; NoComparison>]
type Foo() =
    member x.Bar() = ()
    member x.Qux() = ()
    [<System.Obsolete>]
    member x.HideMe() = ()
    [<CompilerMessage("A warning message",99999,IsError=false,IsHidden=true)>]
    member x.WarnMe() = ()

let foo = new Foo()
foo.  // see intellisense here
Brian
Thanks, Brian I applied this change to clean it up somewhat. As I can see you found both places were I asked the question (I cover my bases).Right now I am going to consider Tomas approach, since he is correct, that I don't need to use types, modules would do.
Thorsten Lorenz