为什么在 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++,吸取教训:-)
我使用 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++,吸取教训:-)