Why is this operator definition possible in Scala?

I use F # and know little about Scala, except that there are often some similarities between these languages. But, looking at the implementation of Akka threads in Scala, I noticed the use of the ~> operator in such a way that this is not possible in F # (unfortunately). I'm not talking about the "~" character, which can only be used in F # at the beginning of unary operators, this is not important. I was struck by the possibility of defining such graphs:

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

Since different elements of the graph have different types (Source, Flow, Sink), it is impossible to define a single operator in F # that will work through them. But I wonder why this is possible in Scala - is it because Scala supports the function function overloading the function (and F # does not)?

UPDATE Fydor Soikin showed several overload methods in F # that could be used to achieve similar syntax when using F #. I tried this and here is what it might look like:

 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 
+7
scala overloading f #
source share
3 answers

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

(1) Method overload.

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

(2) Inheritance.

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

(3) Typical and similar general constructions.

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

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

(4) Implicit conversions.

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

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

Each of them has its own strengths and weaknesses, and all of them are heavily used in various libraries, although the latter somewhat disagree that it is too easy to create surprises. But in order to have the behavior you described, any of them will work.

In practice, in Akka streams, this is actually a mixture of 1-3 of what I can say.

+10
source share

you can define operators as members of a class 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" 
+7
source share

First of all, F # fully supports method overloading:

 type T = static member M (a: int) = a static member M (a: string) = a let x = TM 5 let y = TM "5" 

Then you can actually overload the top-level operator with the first argument using statically permitted type constraints and some clever syntax cheating:

 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 to overload the second argument, you can also do this by enlisting the help of the 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 disadvantage (or, according to some, up) is that all this happens during compilation (see those inline all over the place?). True class types will definitely be better, but you can do a lot with just static type and overload constraints.

And of course, you can also do good inheritance in F #:

 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?

However, this is rarely worth the trouble. In practice, you can usually achieve your ultimate goal with regular functions and maybe just sprinkle on optionally openable user statements, just for added convenience. Overloaded operators may initially look like a shiny cool toy, but they are dangerously easy to abuse. Remember C ++, learn the lessons :-)

+6
source share

All Articles