Reputation: 9991
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
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 inline
s 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
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
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