F#: Преимущества преобразования верхнего уровня функционируют к членским методам?

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

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

let getDecisions hand =
    let (/=/) card1 card2 = matchValue card1 = matchValue card2
    let canSplit() = 
        let isPair() =
            match hand.Cards with
            | card1 :: card2 :: [] when card1 /=/ card2 -> true
            | _ -> false
        not (hasState Splitting hand) && isPair()
    let decisions = [Hit; Stand]
    let split = if canSplit() then [Split] else []
    let doubleDown = if hasState Initial hand then [DoubleDown] else []
    decisions @ split @ doubleDown

к этому:

type Hand
// ...stuff...
member hand.GetDecisions =
    let (/=/) (c1 : Card) (c2 : Card) = c1.MatchValue = c2.MatchValue
    let canSplit() = 
        let isPair() =
            match hand.Cards with
            | card1 :: card2 :: [] when card1 /=/ card2 -> true
            | _ -> false
        not (hand.HasState Splitting) && isPair()
    let decisions = [Hit; Stand]
    let split = if canSplit() then [Split] else []
    let doubleDown = if hand.HasState Initial then [DoubleDown] else []
    decisions @ split @ doubleDown

Теперь, я не сомневаюсь, что я - идиот, но кроме (я предполагаю), создание легче C# interop, что это получало меня? А именно, я нашел пару недостатков, не считая дополнительную работу преобразования (который я не буду считать, так как я, возможно, сделал это этот путь во-первых, я предполагаю, хотя это сделало бы использование F# Интерактивный больше боли). С одной стороны, я теперь больше не могу работать с функцией, "конвейерно обрабатывающей" легко. Я должен был пойти и изменение some |> chained |> calls кому: (some |> chained).Calls и т.д. Кроме того, это, казалось, сделало мою систему типов более немой - тогда как с моей исходной версией, для моей программы не были нужны никакие аннотации типа после преобразования в основном в членские методы, я получил набор ошибок о поисках, являющихся неопределенным в той точке, и я должен был пойти и добавить аннотации типа (пример этого находится в (/=/) выше).

Я надеюсь, что не оторвался слишком сомнительный, поскольку я ценю совет, который я получил, и написание идиоматического кода важно для меня. Мне просто любопытно, почему идиома является способом, которым это :)

Спасибо!

5
задан J Cooper 6 June 2010 в 00:20
поделиться

4 ответа

Преимуществом членов является Intellisense и другие инструменты, которые делают члены доступными для обнаружения. Когда пользователь хочет изучить объект foo, он может набрать foo. и получить список методов этого типа. Члены также легче "масштабируются", в том смысле, что вы не получаете десятки имен, плавающих вокруг на верхнем уровне; с ростом размера программы вам нужно больше имен, чтобы они были доступны только при квалификации (someObj.Method или SomeNamespace.Type или SomeModule.func, а не просто Method/Type/func "плавают свободно").

Как вы видели, есть и недостатки; особенно заметен вывод типа (вам нужно знать тип x априори, чтобы вызвать x.Something); в случае типов и функциональности, которые используются очень часто, может быть полезно предоставить и члены, и модуль функций, чтобы иметь преимущества обоих (так, например, происходит для распространенных типов данных в FSharp.Core).

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

4
ответ дан 18 December 2019 в 11:53
поделиться

Одна из практик, которую я использовал в своем программировании на F# - это использование кода в обоих местах.

Фактическую реализацию естественно поместить в модуль:

module Hand = 
  let getDecisions hand =
    let (/=/) card1 card2 = matchValue card1 = matchValue card2
  ...

и дать "ссылку" в функции-члене/свойстве:

type Hand
  member this.GetDecisions = Hand.getDecisions this

Эта практика также обычно используется в других библиотеках F#, например, реализация матрицы и вектора matrix.fs в powerpack.

На практике, не каждая функция должна быть помещена в оба места. Окончательное решение должно быть основано на знаниях домена.

Что касается верхнего уровня, то практика такова, что функции помещаются в модули, а некоторые из них при необходимости выносятся на верхний уровень. Например, в F# PowerPack, Matrix<'T> находится в пространстве имен Microsoft.FSharp.Math. Удобно сделать ярлык для конструктора матрицы на верхнем уровне, чтобы пользователь мог построить матрицу напрямую, не открывая пространство имен:

[<AutoOpen>]
module MatrixTopLevelOperators = 
    let matrix ll = Microsoft.FSharp.Math.Matrix.ofSeq ll
7
ответ дан 18 December 2019 в 11:53
поделиться

Это сложный вопрос, и оба подхода имеют свои преимущества и недостатки. В целом, я склонен использовать члены при написании более объектно-ориентированного кода в стиле C# и глобальные функции при написании более функционального кода. Однако я не уверен, что могу четко сформулировать, в чем разница.

Я бы предпочел использовать глобальные функции при написании:

  • Функциональных типов данных, таких как деревья/списки и другие общеупотребительные структуры данных, которые хранят некоторый большой объем данных в вашей программе. Если ваш тип предоставляет некоторые операции, которые выражаются как функции высшего порядка (например, List.map), то, вероятно, это такой тип данных.

  • Функциональность, не связанная с типом - бывают ситуации, когда некоторые операции не принадлежат строго одному конкретному типу. Например, когда у вас есть два представления некоторой структуры данных, и вы реализуете преобразование между ними (например, типизированный AST и нетипизированный AST в компиляторе). В этом случае функции - лучший выбор.

С другой стороны, я бы использовал члены при написании:

  • Простых типов, таких как Card, которые могут иметь свойства (например, значение и цвет) и относительно простые методы (или не иметь их), выполняющие некоторые вычисления.

  • Объектно-ориентированные - когда вам нужно использовать объектно-ориентированные концепции, такие как интерфейсы, тогда вам нужно будет написать функциональность в виде членов, поэтому может быть полезно подумать (заранее), будет ли это применимо к вашим типам.

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

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

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

// Using LINQ-like extension methods
list.Where(fun a -> a%3 = 0).Select(fun a -> a * a)
// Using F# list-processing functions
list |> List.filter (fun a -> a%3 = 0) |> List.map (fun a -> a * a)
3
ответ дан 18 December 2019 в 11:53
поделиться

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

type Hand
// ...stuff...

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
    let GetDecisions hand = //...
1
ответ дан 18 December 2019 в 11:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: