Ранее я запросил некоторую обратную связь на своем первом проекте 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
и т.д. Кроме того, это, казалось, сделало мою систему типов более немой - тогда как с моей исходной версией, для моей программы не были нужны никакие аннотации типа после преобразования в основном в членские методы, я получил набор ошибок о поисках, являющихся неопределенным в той точке, и я должен был пойти и добавить аннотации типа (пример этого находится в (/=/)
выше).
Я надеюсь, что не оторвался слишком сомнительный, поскольку я ценю совет, который я получил, и написание идиоматического кода важно для меня. Мне просто любопытно, почему идиома является способом, которым это :)
Спасибо!
Преимуществом членов является Intellisense и другие инструменты, которые делают члены доступными для обнаружения. Когда пользователь хочет изучить объект foo
, он может набрать foo.
и получить список методов этого типа. Члены также легче "масштабируются", в том смысле, что вы не получаете десятки имен, плавающих вокруг на верхнем уровне; с ростом размера программы вам нужно больше имен, чтобы они были доступны только при квалификации (someObj.Method или SomeNamespace.Type или SomeModule.func, а не просто Method/Type/func "плавают свободно").
Как вы видели, есть и недостатки; особенно заметен вывод типа (вам нужно знать тип x
априори, чтобы вызвать x.Something
); в случае типов и функциональности, которые используются очень часто, может быть полезно предоставить и члены, и модуль функций, чтобы иметь преимущества обоих (так, например, происходит для распространенных типов данных в FSharp.Core).
Это типичные компромиссы между "удобством написания сценариев" и "масштабом программной инженерии". Лично я всегда склоняюсь к последнему.
Одна из практик, которую я использовал в своем программировании на 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
Это сложный вопрос, и оба подхода имеют свои преимущества и недостатки. В целом, я склонен использовать члены при написании более объектно-ориентированного кода в стиле 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)
Я обычно избегаю классов в первую очередь из-за того, как члены искалечивают вывод типов. В этом случае иногда удобно сделать имя модуля таким же, как имя типа - здесь пригодится приведенная ниже директива компилятора.
type Hand
// ...stuff...
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
let GetDecisions hand = //...