Подтвердить что ты не робот

F # как пройти эквивалент интерфейса

Я знаю, что буду смеяться, когда увижу этот ответ, но почему-то я этого не вижу.

По какой-то причине он ускользает от меня, как передать несколько функций по одному параметру (из-за отсутствия лучших слов.)

Например, скажем, у меня есть IDoSomething, у которого есть 3 метода:

1.)  DoIt()
2.)  DoItMore()
3.)  DoItMost()

в OO, я бы сделал следующее:

type MyController(something:IDoSomething) =
   let a = something.DoIt()
   let b = something.DoItMore()
   let c = something.DoItMost()

Итак, для F # у меня будет модуль с тремя функциями, упомянутыми выше. Но как мне передать это в мой контроллер? Должен ли я передавать их как отдельную функцию? Мне кажется, что я хочу передать весь модуль хе-хе: -)

4b9b3361

Ответ 1

Этот вопрос, кажется, появляется снова и снова, и как-то принятый ответ часто оказывается "записью функций". Для этого нет разумной мотивации. Записи для данных. Они имеют структурное равенство, которое полностью разрушается, вводя в них функции, поскольку функции не имеют структурного равенства.

Интерфейсы

Итак, что альтернатива интерфейсам в F #?

Ну, если вы абсолютно должны группировать функции вместе, F # позволяет вам определить интерфейсы. Да: интерфейсы:

type IDoSomething =
    abstract DoIt : unit -> unit
    abstract DoItMore : unit -> unit
    abstract DoItMost : unit -> unit

Эта языковая функция существует, поэтому, если вам нужен интерфейс, нет причин придумывать какую-то странную замену.

Но это не работает

Правильно, это не Функционально, но ни одна из них не создает запись функций.

Вопрос заключается в том, существует ли один вездесущий функциональный способ группировки связанных функций. Haskell имеет классы типов и Clojure имеет протоколы (которые для меня немного похожи на классы классов, но тогда я вряд ли являюсь экспертом Clojure),.

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

Функции

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

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

let run foo bar baz = List.map foo >> bar >> List.groupBy baz

Из-за вывода типа даже такой пример бессмыслицы, как описано выше, компилируется. Он имеет тип ('a -> 'b) -> ('b list -> 'c list) -> ('c -> 'd) -> ('a list -> ('d * 'c list) list). Я понятия не имею, что он делает (я только что сделал это), но дело в том, что foo, bar и baz являются функциями. В качестве примера foo является функцией типа 'a -> 'b.

Даже с такой смешной функцией, как run, вы можете применить ее, и это может иметь смысл:

type Parity = Even | Odd
let parity i =
    match i % 2 with
    | 0 -> Even
    | _ -> Odd

open System

let tryParse s =
    match Int32.TryParse s with
    | true, i -> Some i
    | _ -> None
let runP = run tryParse (List.choose id) parity

Функция runP имеет тип string list -> (Parity * int list) list. Что оно делает? Он принимает список строк, отбрасывает те, которые не являются целыми числами, и группирует их по четности (четный/нечетный):

> runP ["Foo"; "1"; "42"; "Bar"; "Baz"; "1337"];;
val it : (Parity * int list) list = [(Odd, [1; 1337]); (Even, [42])]

Итак, это оказалось (вроде) полезным в конце концов!

От ООП до FP

В начале этого разговора я написал: "Если вы абсолютно должны объединить функции вместе". Там причина, которую я написал, если. Даже в OOD, из Принцип разделения сегментов, мы знаем, что мы не должны заставлять клиента зависеть от функций, которые ему не нужны. Передача группы функций клиенту может легко нарушить этот принцип. Чем больше членов определяет интерфейс, тем больше риск нарушения.

В дополнение к этому из Принцип инверсии зависимостей следует, что "клиенты [...] владеют абстрактными интерфейсами" (APPP, глава 11). Другими словами, клиент заявляет, что ему нужно, и интерфейс должен соответствовать этому; это не реализация, определяющая интерфейс.

Как только вы начнете следовать этим и остальным принципам SOLID, вы должны начать понимать, что чем более подробно вы определяете свои интерфейсы, лучше. Логический вывод состоит в том, чтобы определить все интерфейсы только с одним методом. Если клиенту требуется более одного члена, вы всегда можете передать два интерфейса в виде двух аргументов, но вы никогда не сможете удалить элемент из интерфейса, если он уже определен.

Эта расширяемость в двух словах: вы можете расширить, но вы не можете уменьшить.

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

Таким образом, передавайте функции в качестве аргументов. Это функциональный способ сделать это.

Изменить: См. также мой другой ответ (на свободных монадах)

Ответ 2

Используйте тип записи:

type MyFunctions = { 
    DoIt: (unit -> unit); 
    DoItMore: (unit -> unit);
    DoItMost: (unit -> unit);
}

Тогда вы можете

type MyController(functions: MyFunctions) =
    let a = functions.DoIt()
    let b = functions.DoItMore()
    let c = functions.DoItMost()

и вы можете создать экземпляр типа записи со ссылками на функции модуля:

module MyModule =
    let doIt() =
        Console.WriteLine("Do It!")

    let doItMore() =
        Console.WriteLine("Do It More!")

    let doItMost() = 
        Console.WriteLine("Do It Most!")

    let myRecord = { 
        DoIt = doIt; 
        DoItMore = doItMore; 
        DoItMost = doItMost; 
    }

    let controller = 
        MyController(myRecord)

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

type MyController(doIt: (unit -> unit), doItMore: (unit -> unit), doItMost: (unit -> unit))
// ... Etc

Боковое примечание: много unit в F # обычно указывает на плохой дизайн.

Изменить: Марк Семанн ответ - правильный ответ на этот вопрос.

Ответ 3

Большая часть того, что я написал в моем другом ответе, я по-прежнему считаю правильным и идиоматичным для F #. Позже, однако, я узнал, что использование функций или частичного приложения, в то время как тонкое, читаемое, понятное, простое и идиоматическое на многопартийном языке, таком как F #, на самом деле является не строго функциональный.

В двух словах проблема заключается в том, что зависимости, как правило, нечистые, и если вы "вводите" их в код клиента, тогда этот код клиента также становится нечистым, потому что чистый код не может вызвать нечистый код.

В Haskell у вас есть несколько вариантов решения этой проблемы, но не все из них хорошо переводятся на F #. Одна из этих альтернатив делает перевод, по крайней мере, до некоторой степени: свободные монады.

Я не хочу, чтобы этот ответ встречался таким образом, что свободные монады - это идиоматическая замена интерфейсов в F #, но для полноты я также добавляю этот ответ:

Используя F # бесплатный рецепт монады, интерфейс OP становится:

type DoSomethingInstruction<'a> =
| DoIt of 'a
| DoItMore of 'a
| DoItMost of 'a

let private mapI f = function
    | DoIt next     -> DoIt     (next |> f)
    | DoItMore next -> DoItMore (next |> f)
    | DoItMost next -> DoItMost (next |> f)

type DoSomethingProgram<'a> =
| Free of DoSomethingInstruction<DoSomethingProgram<'a>>
| Pure of 'a

let rec bind f = function
    | Free x -> x |> mapI (bind f) |> Free
    | Pure x -> f x

let doIt = Free (DoIt (Pure ()))

let doItMore = Free (DoItMore (Pure ()))

let doItMost = Free (DoItMost (Pure ()))

type DoSomethingBuilder () =
    member this.Bind (x, f) = bind f x
    member this.Return x = Pure x
    member this.ReturnFrom x = x
    member this.Zero () = Pure ()

let doDomething = DoSomethingBuilder ()

Вы можете написать небольшую пробную программу, используя выражение вычисления doSomething:

let p = doDomething {
    do! doIt
    do! doItMore
    do! doItMost }

Кроме того, вы можете "реализовать" "интерфейс", написав интерпретатор:

let rec interpret = function
    | Pure x -> x
    | Free (DoIt next)     -> printfn "Doing it.";      next |> interpret
    | Free (DoItMore next) -> printfn "Doing it more!"; next |> interpret
    | Free (DoItMost next) -> printfn "DOING IT MOST!"; next |> interpret

Теперь вы можете запустить программу:

> interpret p;;
Doing it.
Doing it more!
DOING IT MOST!
val it : unit = ()

Это явно требует большего кода шаблона, но весь код, отличный от интерпретатора, является чистым.

Ответ 4

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

  • в качестве аргументов, curried
  • в качестве аргументов, tupled
  • как значения полей экземпляра типа записи
  • как значения поля (val...) или свойства (конкретный или абстрактный элемент...) конкретного или абстрактного экземпляра типа экземпляра
  • как свойства (абстрактные члены) экземпляра типа интерфейса

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

Вот что говорит эксперт F # 3.0 (стр. 579): "Рекомендация: используйте типы интерфейсов объектов вместо кортежей или записей функций. В главе 5 вы видели различные способы представления словаря операций явно, например, используя кортежи функций или записей функций. В общем, мы рекомендуем использовать для этого типы интерфейсов объектов, поскольку синтаксис, связанный с их реализацией, как правило, более удобен ".