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

Why such operator definition is possible in Scala?

我使用 F#,对 Scala 知之甚少,只是这些语言之间通常有一些相似之处。但是在查看 Scala 中的 Akka Streams 实现时,我注意到运算符 ~> 的使用方式在 F# 中是不可能的(不幸的是)。我不是在谈论只能在 F# 中一元运算符开头使用的符号“~”,这并不重要。给我留下深刻印象的是可以这样定义图形:

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

由于各种图形元素具有不同的类型(Source、Flow、Sink),因此无法在 F# 中定义可跨它们工作的单个运算符。但我想知道为什么这在 Scala 中是可能的——这是因为 Scala 支持 method 函数重载(而 F# 不支持)?

更新。 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

如有必要,您可以将运算符定义为 class 成员

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"

实际上,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 的混合。

首先,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++,吸取教训:-)