为什么在Scala中可以使用这样的运算符定义?

时间:2016-10-14 19:40:00

标签: scala f# overloading

我使用F#并且对Scala了解不多,除了这些语言之间经常有一些相似之处。但是在查看Scala中的Akka Streams实现时,我注意到使用了operator~>以这种方式在F#中不可能(不幸的是)。我不是在谈论符号"〜"只能在一元运算符开头的F#中使用,这并不重要。令我印象深刻的是定义这样的图形的可能性:

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

由于各种图形元素具有不同的类型(源,流,接收器),因此无法在F#中定义单个运算符,这些运算符可以跨越它们。但是我想知道为什么在Scala中这是可能的 - 这是因为Scala支持方法函数重载(而且F#没有' t)?

更新。 Fydor Soikin在F#中展示了几种重载方式,可以用来在使用F#时实现类似的语法。我试过这个,看看它看起来如何:

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

现在我们可以编写以下代码:

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

3 个答案:

答案 0 :(得分:10)

实际上,Scala至少有四种不同的方法可以使它发挥作用。

(1)方法重载。

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

(2)继承。

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

(3)类型和类似的通用结构。

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

(提供所需功能的Streamable[Flow], Streamable[Sink], ...个实例)。

(4)隐含转换。

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

implicit def flowCanStream(f: Flow): Streamable = ???等)。

每一种都有自己的优点和缺点,并且所有这些都在各种库中大量使用,尽管最后因为太容易产生意外而有些失宠。但要拥有你所描述的行为,其中任何一种都可行。

在实践中,在Akka Streams中,它实际上是我能说的1-3的混合物。

答案 1 :(得分:7)

如果需要,您可以将运算符定义为类成员

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"

答案 2 :(得分:6)

首先,F#完全支持方法重载:

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

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

然后,您可以通过第一个参数在静态解析的类型约束和一些聪明的语法技巧的帮助下实际实现顶级运算符重载:

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

最后,如果你真的想通过第二个参数进行重载,你也可以通过从重载的实例方法中获取帮助来实现这一点:

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"

所有这些的缺点(或者,有些人认为,上升)是这一切都发生在编译时(看到那些inline的所有地方?)。真正的类型肯定会更好,但你可以用静态类型约束和重载做很多事情。

当然,你也可以在F#中做好旧的继承:

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

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

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

但是......继承?真的?

那就是说,这很不值得。在实践中,您通常可以通过常规功能实现最终目标,也可以只是一些可选择打开的自定义操作员,只是为了更加方便。重载的操作员起初可能看起来像一个闪亮的酷玩具,但它们很容易过度使用。记住C ++,吸取教训: - )