F #: Преимущества преобразования функций верхнего уровня в методы-члены? - PullRequest
5 голосов
/ 06 июня 2010

Ранее я просил дать отзыв о моем первом проекте 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 #, что меня это задело? В частности, я нашел пару dis преимуществ, не считая дополнительной работы по конверсии (которую я не буду считать, поскольку я мог бы сделать это, во-первых, я полагаю, хотя это имело бы сделал с помощью F # Interactive больше боли). Во-первых, я больше не могу легко работать с функцией конвейерной обработки. Мне пришлось пойти и изменить some |> chained |> calls на (some |> chained).Calls и т. Д. Кроме того, казалось, что моя система типов стала еще тупее - в то время как в моей исходной версии моя программа не нуждалась в аннотациях типов, после преобразования в основном в методы-члены я куча ошибок о неопределенности поисков в этот момент, и мне пришлось пойти и добавить аннотации типов (пример этого в (/=/) выше).

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

Спасибо!

Ответы [ 4 ]

7 голосов
/ 06 июня 2010

Одна практика, которую я использовал в своем программировании на 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
4 голосов
/ 06 июня 2010

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

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

3 голосов
/ 06 июня 2010

Это сложный вопрос, и оба подхода имеют свои преимущества и недостатки. В общем, я склонен использовать члены при написании более объектно-ориентированного кода в стиле 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)
1 голос
/ 18 июня 2010

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

type Hand
// ...stuff...

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
    let GetDecisions hand = //...
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...