Vagif Abilov
Vagif Abilov

Reputation: 9991

Why such operator definition is possible in Scala?

I use F# and don't know much of Scala, except that there are often some similarities between these languages. But while looking at Akka Streams implementation in Scala, I noticed the use of operator ~> in such a way that's not possible in F# (unfortunately). I am not talking about symbol "~" that can only be used in F# in the beginning of unary operators, this is not important. What impressed me is possibility to define graphs like this:

in ~> f1 ~> bcast ~> f2 ~> merge ~> f3 ~> out
            bcast ~> f4 ~> merge

Since various graph elements have different types (Source, Flow, Sink), it's not possible to define a single operator in F# that would work across them. But I wonder why this is possible in Scala - it this because Scala supports method function overloading (and F# doesn't)?

UPDATE. Fydor Soikin showed several ways of overloading in F# that could be used to achieve similar syntax when using F#. I tried this and here how it might look:

type StreamSource<'a,'b,'c,'d>(source: Source<'a,'b>) = 
    member this.connect(flow : Flow<'a,'c,'d>) = source.Via(flow)
    member this.connect(sink: Sink<'a, Task>) = source.To(sink)

type StreamFlow<'a,'b,'c>(flow : Flow<'a,'b,'c>) = 
    member this.connect(sink: Sink<'b, Task>) = flow.To(sink)

type StreamOp = StreamOp with
    static member inline ($) (StreamOp, source: Source<'a,'b>) = StreamSource source
    static member inline ($) (StreamOp, flow : Flow<'a,'b,'c>) = StreamFlow flow

let inline connect (a: ^a) (b: ^b) = (^a : (member connect: ^b -> ^c) (a, b)) 
let inline (>~>) (a: ^a) (b: ^b) = connect (StreamOp $ a) b

Now we can write the following code:

let nums = seq { 11..13 }
let source = nums |> Source.From
let sink = Sink.ForEach(fun x -> printfn "%d" x)
let flow = Flow.FromFunction(fun x -> x * 2)
let runnable = source >~> flow >~> sink

Upvotes: 7

Views: 216

Answers (3)

Fyodor Soikin
Fyodor Soikin

Reputation: 80805

First of all, F# totally does support method overloading:

type T =
    static member M (a: int) = a
    static member M (a: string) = a

let x = T.M 5
let y = T.M "5"

Then, you can actually achieve top-level operator overloading by first argument with the help of statically resolved type constraints and some clever syntactic trickery:

type U = U with
    static member inline ($) (U, a: int) = fun (b: string) -> a + b.Length
    static member inline ($) (U, a: System.DateTime) = fun (b: int) -> string (int a.Ticks + b)
    static member inline ($) (U, a: string) = fun (b: int) -> a.Length + b

let inline (=>) (a: ^a) (b: ^b) = (U $ a) b

let a = 5 => "55"  // = 7
let b = System.DateTime.MinValue => 55  // = "55"
let c = "55" => 7  // = "9"
let d = 5 => "55" => "66" => "77"   // = 11

And finally, if you really want overloading by second argument as well, you can do that too, by enlisting the help from overloaded instance methods:

type I(a: int) = 
    member this.ap(b: string) = a + b.Length
    member this.ap(b: int) = string( a + b )

type S(a: string) = 
    member this.ap(b: int) = b + a.Length
    member this.ap(b: string) = b.Length + a.Length

type W = W with
    static member inline ($) (W, a: int) = I a
    static member inline ($) (W, a: string) = S a

let inline ap (a: ^a) (b: ^b) = (^a : (member ap: ^b -> ^c) (a, b)) 
let inline (==>) (a: ^a) (b: ^b) = ap (W $ a) b

let aa = 5 ==> "55"   // = 7
let bb = "55" ==> 5   // = 7
let cc = 5 ==> "55" ==> 7 ==> "abc" ==> 9  // = "14"

The downside (or, some would argue, the upside) of all this is that it all happens at compile time (see those inlines all over the place?). True type classes would definitely be better, but you can do a lot with just static type constraints and overloading.

And of course, you can do good old inheritance in F# just as well:

type Base() = class end
type A() = inherit Base()
type B() = inherit Base()

let (===>) (a: #Base) (b: #Base) = Base()

let g = A() ===> B() ===> A()

But... Inheritance? Really?

That said, it's rarely worth the trouble. In practice you can usually achieve the end goal via regular functions, and maybe just a sprinkle of optionally-openable custom operators, just for extra convenience. Overloaded operators may look like a shiny cool toy at first, but they're dangerously easy to overuse. Remember C++, learn the lessons :-)

Upvotes: 6

Rex Kerr
Rex Kerr

Reputation: 167901

Actually, Scala has at least four different ways to make it work.

(1) Method overloading.

def ~>(f: Flow) = ???
def ~>(s: Sink) = ???

(2) Inheritance.

trait Streamable { 
  def ~>(s: Streamable) = ???
}
class Flow extends Streamable { ... }
class Sink extends Streamable { ... }

(3) Typeclasses and similar generic constructs.

def ~>[A: Streamable](a: A) = ???

(with Streamable[Flow], Streamable[Sink], ... instances that provide the needed functionality).

(4) Implicit conversions.

def ~>(s: Streamable) = ???

(with implicit def flowCanStream(f: Flow): Streamable = ???, etc.).

Each of these have their own strengths and weaknesses, and all are used heavily in various libraries, though the last has fallen somewhat out of favor due to it being too easy to generate surprises. But to have the behavior you have described, any of these would work.

In practice, in Akka Streams, it's actually a mixture of 1-3 from what I can tell.

Upvotes: 10

desco
desco

Reputation: 16782

you can define operators as class members if necessary

type Base =
    class 
    end

type D1 = 
    class
    inherit Base
    static member (=>) (a: D1, b: D2): D2 = failwith ""
    end

and D2 = 
    class
    inherit Base
    static member (=>) (a: D2, b: D3): D3 = failwith ""
    end

and D3 = 
    class
    inherit Base
    static member (=>) (a: D3, b: string): string = failwith ""
    end

let a: D1 = failwith ""
let b: D2 = failwith ""
let c: D3 = failwith ""

a => b => c => "123"

Upvotes: 7

Related Questions