Шаблон стратегии в F#

В С# у меня есть следующий код:

public class SomeKindaWorker
{
    public double Work(Strategy strat)
    {
        int i = 4;
        // some code ...
        var s = strat.Step1(i);
        // some more code ...
        var d = strat.Step2(s);
        // yet more code ...
        return d;
    }
}

Это фрагмент кода, который может выполнять определенную работу, используя предоставленный object стратегии для заполнения частей реализации. Примечание: как правило, объекты стратегии не содержат состояния; они просто полиморфно обеспечивают реализацию отдельных шагов.

Класс стратегии выглядит так:

public abstract class Strategy
{
    public abstract string Step1(int i);
    public abstract double Step2(string s);
}

public class StrategyA : Strategy
{
    public override string Step1(int i) { return "whatever"; }
    public override double Step2(string s) { return 0.0; }
}

public class StrategyB : Strategy
{
    public override string Step1(int i) { return "something else"; }
    public override double Step2(string s) { return 4.5; }
}

Наблюдение: того же эффекта можно добиться в C# за счет использования лямбда-выражений (и полного избавления от объекта стратегии), но в этой реализации хорошо то, что расширяющие классы имеют свои Step1 и Step2. реализации вместе.

Вопрос. Какова идиоматическая реализация этой идеи в F#?

Мысли:

Я мог бы внедрить отдельные пошаговые функции в функцию работы, подобно идее в наблюдении.

Я также мог бы создать тип, который собирает две функции, и передать значение этого типа через:

type Strategy = { Step1: int -> string; Step2: string -> double }
let strategyA = { Step1 = (fun i -> "whatever"); Step2 = fun s -> 0.0 }
let strategyB = { Step1 = (fun i -> "something else"); Step2 = fun s -> 4.5 }

Это кажется наиболее близким к тому, чего я пытаюсь достичь: он удерживает этапы реализации близко друг к другу, чтобы их можно было рассматривать как связку. Но является ли эта идея (создание типа, содержащего только значения функции) идиоматической в ​​функциональной парадигме? Любые другие мысли?


person CSJ    schedule 08.04.2014    source источник


Ответы (4)


Вот более функциональный подход к вопросу:

type Strategy =
    | StrategyA
    | StrategyB

let step1 i = function
    | StrategyA -> "whatever"
    | StrategyB -> "something else"

let step2 s = function
    | StrategyA -> 0.0
    | StrategyB -> 4.5

let work strategy = 
    let i = 4
    let s = step1 i strategy
    let d = step2 s strategy
    d
person Wesley Wiser    schedule 09.04.2014
comment
Это выглядит одновременно и чрезвычайно многообещающе, и крайне устрашающе. С одной стороны, синтаксис легкий и понятный; с другой стороны, полная картина любой заданной стратегии распределяется между case-операторами различных функций. Можете ли вы прокомментировать это -- это другая группировка функций, к которой мне просто нужно привыкнуть? - person CSJ; 09.04.2014
comment
@CSJ На мой взгляд, это одна из определяющих характеристик FP и OOP. Подробнее об этом я рассказываю здесь: programmers.stackexchange.com/a/209616/4132 - person Wesley Wiser; 09.04.2014
comment
Этот ответ (и связанный пост) чрезвычайно описательный и полезный. Один момент, который выделяется в связанном посте, заключается в том, что ООП поощряет объединение данных и поведения, в то время как функциональное программирование поощряет их разделение. Если бы вы включили это в свой ответ в дополнение к ссылке на другой свой пост, я бы немедленно принял это. :) - person CSJ; 09.04.2014

Здесь следует использовать выражения объекта F#:

type IStrategy =
    abstract Step1: int -> string
    abstract Step2: string -> double

let strategyA =
    { new IStrategy with
        member x.Step1 _ = "whatever"
        member x.Step2 _ = 0.0 }

let strategyB =
    { new IStrategy with
        member x.Step1 _ = "something else"
        member x.Step2 _ = 4.5 }

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

Ваш подход с использованием записей функций хорош, но не самый идиоматический. Вот что предлагает Руководство по проектированию компонентов F# (стр. 9):

В F# существует несколько способов представления словаря операций, например использование кортежей функций или записей функций. В общем, мы рекомендуем вам использовать интерфейсные типы для этой цели.

ИЗМЕНИТЬ:

Обновления записей с использованием with — это здорово, но IntelliSense не очень хорошо работает, когда поля записи являются функциями. Используя интерфейсы, вы можете дополнительно настраивать, передавая параметры внутри выражений объекта, например.

let createStrategy label f =
    { new IStrategy with 
        member x.Step1 _ = label
        member x.Step2 s =  f s }

или прибегнуть к реализации интерфейса с использованием interface IStrategy with (это было бы так же, как подход C #), когда вам нужна большая расширяемость.

person pad    schedule 08.04.2014
comment
Это выглядит довольно гладко. Однако один комментарий: типы, лежащие в основе значений StrategyA и StrategyB, теперь анонимны и не могут быть повторно использованы (т.е. унаследованы); тогда как в подходе записи вы могли бы создать новую стратегию из существующей, заменив некоторые из ее шагов, используя синтаксис with. Хотя мне нравится, что значение this/x доступно для использования, если оно понадобится. - person CSJ; 08.04.2014
comment
Аккуратно, это похоже на анонимные классы в Java, которых мне очень не хватало, когда я начинал с C# (и не понимаю, почему это никогда не было реализовано там; так удобно). - person Dax Fohl; 08.04.2014

Вы упоминаете возможность простого использования лямбда-выражений в С#. Для стратегий с несколькими шагами это часто идиоматично. Это может быть очень удобно:

let f step1 step2 = 
    let i = 4
    // ...
    let s = step1 i
    // ...
    let d = step2 s
    //  ...
    d

Нет необходимости в определениях интерфейса или выражениях объектов; предполагаемых типов step1 и step2 достаточно. В языках без функций высшего порядка (в которых, как я полагаю, был изобретен паттерн стратегии) ​​у вас нет этой опции, и вместо этого вам нужны, например, интерфейсы.

Функцию f здесь, по-видимому, не волнует, связаны ли step1 и step2. Но если вызывающая сторона это делает, ничто не мешает ему объединить их в структуру данных. Например, используя ответ @pad,

let x = f strategyA.Step1 strategyA.Step2
// val it = 0.0 

В общем, «идиоматический способ» зависит от того, почему вы в первую очередь рассматриваете шаблон стратегии. Паттерн стратегии предназначен для сшивания функциональности; функции более высокого порядка также часто очень хороши для этого.

person Søren Debois    schedule 08.04.2014
comment
Я думаю, что в этом случае я предпочел бы сохранить функции step1 и step2 связанными с точки зрения f. Я бы с большей вероятностью объединил пару функций в виде кортежа, чем предоставлял бы каждую по отдельности. В свою очередь, я, скорее всего, затем соберу их в типе записи (моя первоначальная мысль) или объектном выражении (@идея пэда). - person CSJ; 08.04.2014
comment
Я думаю, что именно здесь лучшее решение зависит от того, что на самом деле делает f. Во многих случаях, особенно когда компоненты стратегии функциональны (без побочных эффектов), они не обязательно должны быть связаны между собой. Но, конечно, далеко не все. - person Søren Debois; 08.04.2014

Объектные выражения поддерживают только один интерфейс за раз. Если вам нужно два, используйте определение типа.

type IStrategy =
    abstract Step1: int -> string
    abstract Step2: string -> double

type strategyA() =
    let mutable observers = []

    interface System.IObservable<string> with
        member observable.Subscribe(observer)  =
            observers <- observer :: observers
            { new System.IDisposable with
                 member this.Dispose() =
                    observers <- observers |> List.filter ((<>) observer)}

    interface IStrategy with
        member x.Step1 _ = 
            let result = "whatever"
            observers |> List.iter (fun observer -> observer.OnNext(result))
            result
        member x.Step2 _ = 0.0

type SomeKindaWorker() =
    member this.Work(strategy : #IStrategy) =
        let i = 4
        // some code ...
        let s = strategy.Step1(i)
        // some more code ...
        let d = strategy.Step2(s)
        // yet more code ...
        d

let strat = strategyA()
let subscription = printfn "Observed: %A" |> strat.Subscribe
SomeKindaWorker().Work(strat) |> printfn "Result: %A"
subscription.Dispose()

Еще один паттерн, который я часто вижу, — это возврат объектных выражений из функций.

let strategyB(setupData) =
    let b = 3.0 + setupData

    { new IStrategy with
        member x.Step1 _ = "something else"
        member x.Step2 _ = 4.5 + b }

Это позволяет вам инициализировать вашу стратегию.

SomeKindaWorker().Work(strategyB(2.0)) |> printfn "%A"
person gradbot    schedule 08.04.2014