Почему такое определение оператора возможно в Scala?

#scala #f# #overloading

#scala #f# #перегрузка

Вопрос:

Я использую F # и мало что знаю о Scala, за исключением того, что между этими языками часто есть некоторые сходства. Но, рассматривая реализацию Akka Streams в Scala, я заметил использование operator ~> таким образом, что это невозможно в F # (к сожалению). Я не говорю о символе «~», который может использоваться только в F # в начале унарных операторов, это не важно. Что меня впечатлило, так это возможность определять подобные графики:

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

Поскольку различные элементы графа имеют разные типы (источник, поток, приемник), невозможно определить один оператор в F #, который работал бы с ними. Но мне интересно, почему это возможно в Scala — это потому, что Scala поддерживает перегрузку функции метода (а F # нет)?

Обновить. Федор Сойкин показал несколько способов перегрузки в 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
 

Комментарии:

1. F # полностью поддерживает перегрузку метода.

Ответ №1:

На самом деле, у 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, это на самом деле смесь 1-3, насколько я могу судить.

Комментарии:

1. Спасибо за отличное объяснение.

Ответ №2:

при необходимости вы можете определить операторы как члены класса

 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"
 

Ответ №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 s повсюду?). Классы истинного типа определенно были бы лучше, но вы можете многое сделать, используя только ограничения статического типа и перегрузку.

И, конечно, вы также можете использовать старое доброе наследование в 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 , извлекайте уроки 🙂

Комментарии:

1. Вы, конечно, правы насчет перегрузки метода. То, что я имею в виду, было перегрузкой функции и неправильно называлось перегрузкой метода. По-видимому, невозможность определить аналогичные (для Scala) операторы F # для потоков Akka связана с тем фактом, что Akka был перенесен из .NET на C #, поэтому очень мало свободы в том, что можно сделать в F # в Akka. СЕТЕВЫЕ классы. Если бы это был порт на F # в первую очередь, тогда, я думаю, было бы больше возможностей.

2. Вы также можете выполнять перегрузку функций (в некоторой степени), как я показал в приведенных выше примерах.

3. Да, вы снова правы. Это возможно через определение типа.

4. Потратил больше времени, играя с вашими предложениями, и на самом деле они работают очень хорошо, с вашим предложением о перегрузке второго аргумента я могу добиться синтаксиса, аналогичного тому, как они используют его в Scala. Большое спасибо!