2
votes

I have looked far and wide for an answer to this but have exhausted all attempts.

I want to override a custom operator already defined in a project, the classic compose >=> operator, such that if it is used with my class type, it will use its static operator overload instead but every time I use the class with the >=> operator, it gives me error that my class is not compatible with global operator definition. to help explain:

type Handler = context -> context option // context just placeholder for type

let (>=>) (a:Handler) (b:Handler) = fun ctx -> match a ctx with | Some u -> b u | None

type Node(key) =
...
member static (>=>) (p:Node,h:Handler) = ...
member static (>=>) (p:Node,pl Node list) = ...

This is so that I may write code that can wrap the composed handlers like

// val Node = Node (overloaded >=>) Handler (overloaded >=>) Handler (overloaded >=>) [ ... ]
let node = Node "key1" >=> handler1 >=> hander2 >=> [
                                Node "key2" >=> handler3
// val Handler = Handler (global >=>) Handler
let handler3 = handler1 >=> handler2 
                                ]

But unfortunately, the static method overloads on the class cannot override the global operator and take precedence ... is there anything I can do to override the global operator to make this work!? ... I know I can change my Handle from a type augmentation to a full class implementation, remove global operator and use only static overrides on the two classes but to integrate with the existing framework i'm looking at, needs to be a function of this format ...

I was thinking of perhaps overloading FSharpFunc> with static member (>=>) but in f# this would need to be declared in the original declaration location which is off limits.

I looked into claimed way in c# to override with overload operator using virtual methods but this didn't seem to work.

All this Hacking (for want of a better word) is to maintain the compose operator format while using a Node class to store these handlers to build into a Node tree. The fact that the child lists are optional means that although I could overload the constructor using tuples of (key, composedHandlers) / (key, composedHandlers, ChildList), this would be too ugly and wrapped to be accepted ie:

let node1 = Node ("key1", handle1 >=> handle2 , [
                                        Node ("key2",handle3 >=> handle4, [
                                                                    Node ("key3",handle5)
                                                                    ])
                                                ])

I could change the class static operator to something like => / ==> but then it causes confusion as to when to use >=> or use =>, it's too much to ask a user to figure out if handlers are feeding into a node or are they purely composing two handlers into one.

If statically resolved type conditions were not locked to only Core I could do something like the following ... but not allowed:

let (>=>) (a:^T) (b:Handler)
     when ^T : Handler   = ...
     when ^T : Node      = ...
     when ^T : Node list = ...

The issue boils down to, How can I make operator type conditional or how can I make an overloaded class operator override a global operator so that I can use both together depending on the infix types.

---EDIT---

@Gustavo linked post was exactly what I was looking for:

type ComposeExtension = ComposeExtension with
    static member        (?<-) (ComposeExtension, (a:PathNode) , (b:HttpHandler)) = a.AddHandler b
    static member        (?<-) (ComposeExtension, (a:PathNode) , (b:PathNode list)) = a.AddChildPaths b
    static member inline (?<-) (ComposeExtension, a , b) = a >=> b
let inline (>=>) a b = (?<-) ComposeExtension a b
2

2 Answers

3
votes

My answer to this question shows how to rewire a global operator.

If you post a minimal repro, I can show you how to apply it to your case.

Having said that, I would recommend to use FSharpPlus which contains already a better global definition of the operator >=> and all you have to do to make it work with your class is to define Bind and Return, like this:

static member Return a = ...
static member Bind  (x, f) = ...

Again, if you show me your code I can put more details.

0
votes

I don't think there is a way to do exactly what you want, but if your goal is to produce a nice-looking DSL, you can go a slightly different route: wrap both the "just node" case and the "node with handlers applied" case into a DU, then define the operator on that DU:

type Handler = context -> context option
type Node = Node of key:string
type Composable = CNode of Node | CHandled of Handler

let node key = CNode (Node key)

let inline (>=>) (a:Composable) (b:Handler) = 
    match a with
    | CNode n -> ...
    | CHandled h -> ...

// Usage:
let handler1 = fun ctx -> ...
let handler2 = fun ctx -> ...

let a = node "abc" >=> handler1 >=> handler2

This works syntactically and matches your original signatures, but I must say that it looks a bit nonsensical to me, and this is because I don't quite understand what your ultimate goal is: what is Node? why does it need to be "handled"? what is the result of handling? from your code it looks like the result of "handling" is yet another "handler", but is that correct? And so on.

If you can clarify your domain better, I'm sure we can come up with a better solution.