#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. Большое спасибо!