Рекомендации по проектированию компонентов F#

Этот документ представляет собой набор рекомендаций по проектированию компонентов для программирования F# на основе рекомендаций по проектированию компонентов F#, версии 14, Microsoft Research и версии, которая была изначально курирована и поддерживается фондом F# Software Foundation.

В этом документе предполагается, что вы знакомы с программированием F#. Большое спасибо сообществу F# за их вклады и полезные отзывы о различных версиях этого руководства.

Обзор

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

  • Слой в проекте F# с внешними потребителями в этом проекте.
  • Библиотека, предназначенная для использования кодом F# по границам сборки.
  • Библиотека, предназначенная для использования любым языком .NET через границы сборки.
  • Библиотека, предназначенная для распространения через репозиторий пакетов, например NuGet.

Методы, описанные в этой статье, соответствуют пяти принципам хорошего кода F#, поэтому используют как функциональное, так и объектное программирование соответствующим образом.

Независимо от методологии, конструктор компонентов и библиотек сталкивается с рядом практических и прозаичных проблем при попытке создать API, который проще всего использовать разработчиками. Добросовестное применение рекомендаций по проектированию библиотеки .NET поможет вам создать согласованный набор API, которые приятно использовать.

Общие рекомендации

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

Сведения о рекомендациях по проектированию библиотеки .NET

Независимо от того, какой код F# вы делаете, важно иметь рабочие знания о рекомендациях по проектированию библиотек .NET. Большинство других программистов F# и .NET будут знакомы с этими рекомендациями и ожидают, что код .NET соответствует им.

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

Добавление комментариев к XML-документации в код

XML-документация по общедоступным API гарантирует, что пользователи могут получить большую функцию Intellisense и Quickinfo при использовании этих типов и членов и включить создание файлов документации для библиотеки. См. XML-документацию по различным xml-тегам, которые можно использовать для дополнительной разметки в комментариях xmldoc.

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

Можно использовать короткие xml-комментарии формы (/// comment) или стандартные XML-комментарии (///<summary>comment</summary>).

Рекомендуется использовать явные файлы подписи (FSI) для стабильных API библиотеки и компонентов.

Использование явных файлов сигнатур в библиотеке F# предоставляет краткий обзор общедоступного API, который помогает обеспечить полное общедоступное представление библиотеки и обеспечивает четкое разделение между общедоступной документацией и внутренними сведениями о реализации. Файлы подписи добавляют трения к изменению общедоступного API, требуя внесения изменений как в файлы реализации, так и подписи. В результате файлы подписи обычно следует вводить только в том случае, если API стал сплошным и больше не ожидается существенно измениться.

Следуйте рекомендациям по использованию строк в .NET

Следуйте рекомендациям по использованию строк в руководстве по .NET, когда область проекта гарантирует его. В частности, явно указывая культурное намерение в преобразовании и сравнении строк (где применимо).

Рекомендации по библиотекам F#-лиц

В этом разделе представлены рекомендации по разработке общедоступных библиотек F#; То есть библиотеки, предоставляющие общедоступные API, которые предназначены для использования разработчиками F#. Существуют различные рекомендации по проектированию библиотек, применимые специально к F#. В отсутствие конкретных рекомендаций, приведенных ниже, руководство по проектированию библиотеки .NET является резервным руководством.

Соглашения об именах

Использование соглашений об именовании и заглавной буквы .NET

В следующей таблице приведены соглашения об именовании и заглавной букве .NET. Существуют небольшие дополнения, которые также включают конструкции F#. Эти рекомендации особенно предназначены для ИНТЕРФЕЙСов API, которые пересекаются за пределами границ F#to-F#, подходя к идиомам из .NET BCL и большинства библиотек.

Конструкция Регистр Часть Примеры Примечания.
Конкретные типы PascalCase Существительное/ прилагательное List, Double, Complex Конкретные типы — это структуры, классы, перечисления, делегаты, записи и объединения. Хотя имена типов традиционно являются строчными в OCaml, F# принял схему именования .NET для типов.
Библиотеки DLL PascalCase Fabrikam.Core.dll
Теги объединения PascalCase Имя существительное Некоторые, добавление, успех Не используйте префикс в общедоступных API. При необходимости используйте префикс при внутренней, например "type Teams = TAlpha | ТБ eta | TDelta".
Событие PascalCase Команда ValueChanged / ValueChanging
Исключения PascalCase WebException Имя должно заканчиваться именем "Exception".
Поле PascalCase Имя существительное CurrentName
Типы интерфейсов PascalCase Существительное/ прилагательное IDisposable Имя должно начинаться с "I".
Способ PascalCase Команда ToString
Пространство имен PascalCase Microsoft.FSharp.Core Обычно используется <Organization>.<Technology>[.<Subnamespace>], хотя и удаляет организацию, если технология не зависит от организации.
Параметры верблюдю Имя существительное typeName, преобразование, диапазон
let values (internal) верблюдьий Регистр или ПаскальCase Существительное/ глагол getValue, myTable
let values (external) верблюдьий Регистр или ПаскальCase Существительное/глагол List.map, Dates.Today Значения, связанные с помощью разрешений, часто являются общедоступными при выполнении традиционных шаблонов функционального проектирования. Однако обычно используйте PascalCase, если идентификатор можно использовать на других языках .NET.
Свойство PascalCase Существительное/ прилагательное IsEndOfFile, BackColor Логические свойства обычно используют Is и Can и должны быть утвердительными, как и в IsEndOfFile, а не IsNotEndOfFile.

Избегайте аббревиаций

Рекомендации .NET препятствуют использованию аббревиаций (например, "использовать OnButtonClick вместо OnBtnClick"). Распространенные аббревиаты, такие как Async "асинхронные", допускаются. Иногда это руководство игнорируется для функционального программирования; Например, List.iter использует аббревиацию для "итерации". По этой причине использование аббревиаций, как правило, допускается в большей степени в программировании F#to-F#, но, как правило, следует избегать в проектировании общедоступных компонентов.

Избегайте конфликтов имен регистра

Рекомендации .NET говорят, что только регистр не может использоваться для диамбигуации конфликтов имен, так как некоторые клиентские языки (например, Visual Basic) не учитывает регистр.

Используйте акронимы при необходимости

Акронимы, такие как XML, не являются сокращенными и широко используются в библиотеках .NET в некапитализированной форме (XML). Следует использовать только известные, широко признанные акронимы.

Использование PascalCase для универсальных имен параметров

Используйте PascalCase для универсальных имен параметров в общедоступных API, включая библиотеки с интерфейсом F#. В частности, используйте такие имена, как , T2UT1для произвольных универсальных параметров, и когда конкретные имена имеет смысл, то для библиотек f#-лиц используются такие имена, как KeyValueT, Arg (но не например, ). TKey

Используйте PascalCase или верблюдье Регистр для общедоступных функций и значений в модулях F#

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

Конструктор объектов, типов и модулей

Использование пространств имен или модулей для хранения типов и модулей

Каждый файл F# в компоненте должен начинаться с объявления пространства имен или объявления модуля.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Различия между использованием модулей и пространств имен для упорядочивания кода на верхнем уровне приведены ниже.

  • Пространства имен могут охватывать несколько файлов
  • Пространства имен не могут содержать функции F#, если они не находятся в внутреннем модуле
  • Код для любого заданного модуля должен содержаться в одном файле.
  • Модули верхнего уровня могут содержать функции F# без необходимости внутреннего модуля

Выбор между пространством имен верхнего уровня или модулем влияет на скомпилированную форму кода, и, таким образом, повлияет на представление с других языков .NET, если API в конечном итоге будет использоваться за пределами кода F#.

Использование методов и свойств для операций, встроенных в типы объектов

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

type HardwareDevice() =

    member this.ID = ...

    member this.SupportedProtocols = ...

type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =

    member this.Add(key, value) = ...

    member this.ContainsKey(key) = ...

    member this.ContainsValue(value) = ...

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

Использование классов для инкапсулировать изменяемое состояние

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

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

Используйте типы интерфейсов для представления набора операций. Это предпочтительнее для других вариантов, таких как кортежи функций или записей функций.

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

В предпочтениях:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

Интерфейсы являются основными понятиями в .NET, которые можно использовать для достижения того, что Functors обычно дает вам. Кроме того, их можно использовать для кодирования экзистенциальных типов в программе, какие записи функций не могут быть.

Использование модуля для группировки функций, которые действуют в коллекциях

При определении типа коллекции рекомендуется предоставить стандартный набор операций, например CollectionType.map и CollectionType.iter) для новых типов коллекций.

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

Если вы включаете такой модуль, следуйте стандартным соглашениям об именовании функций, найденных в FSharp.Core.

Использование модуля для группирования функций для распространенных канонических функций, особенно в математических и DSL-библиотеках

Например, Microsoft.FSharp.Core.Operators автоматически открывается коллекция функций верхнего уровня (например abs , и sin), предоставляемых FSharp.Core.dll.

Аналогичным образом, библиотека статистики может включать модуль с функциями erf и erfc, где этот модуль предназначен для явного или автоматического открытия.

Рекомендуется использовать RequireQualifiedAccess и тщательно применять атрибуты AutoOpen

Добавление атрибута [<RequireQualifiedAccess>] в модуль указывает, что модуль не может быть открыт, и что ссылки на элементы модуля требуют явного квалифицированного доступа. Например, модуль Microsoft.FSharp.Collections.List имеет этот атрибут.

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

Настоятельно рекомендуется использовать [<RequireQualifiedAccess>] атрибут для пользовательских модулей, расширяющих FSharp.Core предоставленные (например, , Array), так как SeqListэти модули часто используются в коде F# и [<RequireQualifiedAccess>] определяются на них; в целом, не рекомендуется определять пользовательские модули, не имеющие атрибута, когда такие модули теняются или расширяют другие модули с атрибутом.

Добавление атрибута [<AutoOpen>] в модуль означает, что модуль будет открыт при открытии содержащего пространства имен. Атрибут [<AutoOpen>] также может применяться к сборке, чтобы указать модуль, который автоматически открывается при ссылке на сборку.

Например, библиотека статистики MathsHeaven.Statistics может содержать module MathsHeaven.Statistics.Operators содержащие функции erf и erfc. Разумно пометить этот модуль как [<AutoOpen>]. Это означаетopen MathsHeaven.Statistics, что этот модуль также откроется и приведет имена erf и erfc в область. Еще одним хорошим способом является использование [<AutoOpen>] модулей, содержащих методы расширения.

Чрезмерное использование [<AutoOpen>] загрязненных пространств имен, а атрибут должен использоваться с осторожностью. Для конкретных библиотек в определенных доменах разумное использование [<AutoOpen>] может привести к повышению удобства использования.

Рассмотрите возможность определения членов оператора в классах, где использование известных операторов подходит

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

type Vector(x: float) =

    member v.X = x

    static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)

    static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)

let v = Vector(5.0)

let u = v * 10.0

Это руководство соответствует общим рекомендациям .NET для этих типов. Однако это может быть дополнительно важно в кодировании F#, так как это позволяет использовать эти типы в сочетании с функциями и методами F# с ограничениями элементов, такими как List.sumBy.

Рекомендуется использовать скомпилированное имя для предоставления. Понятное для NET имя для других потребителей языка .NET

Иногда может потребоваться назвать что-то в одном стиле для потребителей F# (например, статический элемент в нижнем регистре, чтобы он отображался как если бы это была функция, привязанная к модулю), но имеет другой стиль для имени при компиляции в сборку. Атрибут можно использовать [<CompiledName>] для предоставления другого стиля для кода, отличного от F#, потребляющего сборку.

type Vector(x:float, y:float) =

    member v.X = x
    member v.Y = y

    [<CompiledName("Create")>]
    static member create x y = Vector (x, y)

let v = Vector.create 5.0 3.0

С помощью [<CompiledName>]можно использовать соглашения об именовании .NET для потребителей сборки, отличных от F#.

Используйте перегрузку методов для функций-членов, если это обеспечивает более простой API

Перегрузка методов — это мощный инструмент для упрощения API, который может потребоваться выполнить аналогичные функции, но с различными параметрами или аргументами.

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

В F#чаще всего перегружается по количеству аргументов, а не типам аргументов.

Скрытие представлений типов записей и союзов, если дизайн этих типов, скорее всего, будет развиваться

Избегайте выявления конкретных представлений объектов. Например, конкретное представление значений DateTime не отображается внешним общедоступным API проектирования библиотеки .NET. Во время выполнения среда CLR знает зафиксированную реализацию, которая будет использоваться во время выполнения. Однако скомпилированный код не выбирает зависимости от конкретного представления.

Избегайте использования наследования реализации для расширяемости

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

Подписи функций и элементов

Использование кортежей для возвращаемых значений при возврате небольшого числа нескольких несвязанных значений

Ниже приведен хороший пример использования кортежа в возвращаемом типе:

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

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

Использование Async<T> асинхронного программирования на границах API F#

Если имеется соответствующая синхронная операция с именем Operation , которая возвращает значение T, то асинхронная операция должна быть названа AsyncOperation , если она возвращается Async<T> или OperationAsync возвращается Task<T>. Для часто используемых типов .NET, которые предоставляют методы Begin/End, рекомендуется использовать для Async.FromBeginEnd записи методов расширения в качестве фасада для предоставления модели асинхронного программирования F# этим API .NET.

type SomeType =
    member this.Compute(x:int): int =
        ...
    member this.AsyncCompute(x:int): Async<int> =
        ...

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        ...

Исключения

Сведения об использовании исключений, результатов и параметров см. в статье "Управление ошибками".

Члены расширения

Тщательно примените члены расширения F# в компонентах F#to-F#

Члены расширения F# обычно должны использоваться только для операций, которые находятся в закрытии встроенных операций, связанных с типом в большинстве его режимов использования. Одним из распространенных способов является предоставление API- интерфейсов, которые являются более идиоматичными для F# для различных типов .NET:

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        Async.FromBeginEnd(this.BeginReceive, this.EndReceive)

type System.Collections.Generic.IDictionary<'Key,'Value> with
    member this.TryGet key =
        let ok, v = this.TryGetValue key
        if ok then Some v else None

Типы союзов

Используйте дискриминированные объединения вместо иерархий классов для данных, структурированных в виде дерева

Структуры, похожие на дерево, определяются рекурсивно. Это неловко с наследованием, но элегантно с дискриминированными профсоюзами.

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

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

Использование [<RequireQualifiedAccess>] типов объединения, имена вариантов которых недостаточно уникальны

Вы можете найти себя в домене, где одно и то же имя является лучшим именем для различных вещей, таких как случаи дискриминированного союза. Вы можете использовать [<RequireQualifiedAccess>] для диамбигуации имен регистров, чтобы избежать возникновения запутанных ошибок из-за тени, зависящей от порядка инструкций open

Скрытие представлений дискриминированных профсоюзов для двоичных совместимых API, если дизайн этих типов, скорее всего, будет развиваться

Типы профсоюзов используют формы сопоставления шаблонов F# для краткой модели программирования. Как упоминание ранее, следует избегать выявления конкретных представлений данных, если проектирование этих типов, скорее всего, будет развиваться.

Например, представление дискриминированного объединения может быть скрыто с помощью частного или внутреннего объявления или с помощью файла сигнатуры.

type Union =
    private
    | CaseA of int
    | CaseB of string

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

Активные шаблоны предоставляют альтернативный способ предоставления потребителям F# сопоставления шаблонов, избегая прямого предоставления типов профсоюзов F#.

Встроенные функции и ограничения элементов

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

Ограничения арифметических элементов и ограничения сравнения F#являются стандартом для программирования F#. Рассмотрим следующий пример кода:

let inline highestCommonFactor a b =
    let rec loop a b =
        if a = LanguagePrimitives.GenericZero<_> then b
        elif a < b then loop a (b - a)
        else loop (a - b) b
    loop a b

Тип этой функции выглядит следующим образом:

val inline highestCommonFactor : ^T -> ^T -> ^T
                when ^T : (static member Zero : ^T)
                and ^T : (static member ( - ) : ^T * ^T -> ^T)
                and ^T : equality
                and ^T : comparison

Это подходящая функция для общедоступного API в математической библиотеке.

Избегайте использования ограничений членов для имитации классов типов и ввода утки

Можно имитировать "даковые вводы" с помощью ограничений элементов F#. Однако члены, которые используют это, обычно не должны использоваться в проектах библиотек F#to-F#. Это связано с тем, что проекты библиотек на основе незнакомых или нестандартных неявных ограничений, как правило, приводят к тому, что пользовательский код становится негибким и привязан к одному конкретному шаблону платформы.

Кроме того, существует хорошая вероятность того, что такое ограничение элементов может привести к очень длительному времени компиляции.

Определения операторов

Избегайте определения пользовательских символьных операторов

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

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

Единицы измерения

Тщательно используйте единицы измерения для добавленной безопасности типов в коде F#

Дополнительные сведения о вводе единиц измерения удаляются при просмотре другими языками .NET. Помните, что компоненты, инструменты и отражение .NET будут видеть типы-sans-units. Например, потребители C# будут видеть float вместо float<kg>этого.

Сокращенные обозначения типов

Тщательно используйте сокращения типов для упрощения кода F#

Компоненты , инструменты и отражение .NET не будут видеть сокращенные имена типов. Значительное использование сокращенных типов также может сделать домен более сложным, чем на самом деле, что может запутать потребителей.

Избегайте сокращений типов для общедоступных типов, члены и свойства которых должны быть внутренне разными для доступных для сокращенного типа

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

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

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

Однако логические операции с точками нотации этого типа не совпадают с операциями на карте, например, разумно, что оператор map[key] подстановки возвращает пустой список, если ключ не находится в словаре, а не вызывает исключение.

Рекомендации по библиотекам для использования с других языков .NET

При разработке библиотек для использования на других языках .NET важно придерживаться рекомендаций по проектированию библиотек .NET. В этом документе эти библиотеки помечены как библиотеки vanilla .NET, а не библиотеки F#, использующие конструкции F# без ограничений. Проектирование библиотек vanilla .NET означает предоставление знакомых и идиоматических API в соответствии с остальными платформа .NET Framework путем минимизации использования конструкций F#в общедоступном API. Правила описаны в следующих разделах.

Конструктор пространства имен и типов (для библиотек для использования на других языках .NET)

Применение соглашений об именовании .NET к общедоступному API компонентов

Обратите особое внимание на использование сокращенных имен и рекомендаций по прописи .NET.

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

Используйте пространства имен, типы и члены в качестве основной организационной структуры для компонентов.

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

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

Статические типы следует предпочтительнее по сравнению с модулями, так как они позволяют в будущем эволюции API использовать перегрузку и другие концепции проектирования API .NET, которые могут не использоваться в модулях F#.

Например, вместо следующего общедоступного API:

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

Вместо этого рассмотрим:

namespace Fabrikam

[<AbstractClass; Sealed>]
type Utilities =
    static member Name = "Bob"
    static member Add(x,y) = x + y
    static member Add(x,y,z) = x + y + z

Используйте типы записей F# в API ванильной .NET, если дизайн типов не будет развиваться.

Типы записей F# компилируются в простой класс .NET. Они подходят для некоторых простых, стабильных типов в API. Рекомендуется использовать [<NoEquality>] атрибуты для [<NoComparison>] подавления автоматического создания интерфейсов. Кроме того, не используйте изменяемые поля записей в API-интерфейсах .NET для ванили, так как они предоставляют общедоступное поле. Всегда учитывайте, будет ли класс предоставлять более гибкий вариант для дальнейшего развития API.

Например, следующий код F# предоставляет общедоступный API потребителю C#:

F#:

[<NoEquality; NoComparison>]
type MyRecord =
    { FirstThing: int
        SecondThing: string }

C#.

public sealed class MyRecord
{
    public MyRecord(int firstThing, string secondThing);
    public int FirstThing { get; }
    public string SecondThing { get; }
}

Скрытие представления типов объединения F# в ванильных API .NET

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

При проектировании API vanilla .NET рекомендуется скрыть представление типа объединения с помощью частного объявления или файла подписи.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

Вы также можете расширить типы, которые используют представление объединения внутри членов для предоставления требуемого. API с интерфейсом NET.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

    /// A public member for use from C#
    member x.Evaluate =
        match x with
        | And(a,b) -> a.Evaluate && b.Evaluate
        | Not a -> not a.Evaluate
        | True -> true

    /// A public member for use from C#
    static member CreateAnd(a,b) = And(a,b)

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

Существует множество различных платформ, доступных в .NET, таких как WinForms, WPF и ASP.NET. Соглашения об именовании и проектировании для каждого из них следует использовать, если вы разрабатываете компоненты для использования в этих платформах. Например, для программирования WPF внедряйте шаблоны проектирования WPF для классов, которые вы разрабатываете. Для моделей в программировании пользовательского интерфейса используйте такие шаблоны проектирования, как события и коллекции на основе уведомлений, такие как найденные в System.Collections.ObjectModel.

Проектирование объектов и элементов (для библиотек для использования с других языков .NET)

Использование атрибута CLIEvent для предоставления событий .NET

DelegateEvent Создайте объект с определенным типом делегата .NET, который принимает объект и EventArgs (а не EventFSharpHandler тип по умолчанию), чтобы события публикулись знакомым образом на других языках .NET.

type MyBadType() =
    let myEv = new Event<int>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

type MyEventArgs(x: int) =
    inherit System.EventArgs()
    member this.X = x

    /// A type in a component designed for use from other .NET languages
type MyGoodType() =
    let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

Предоставление асинхронных операций в качестве методов, возвращающих задачи .NET

Задачи используются в .NET для представления активных асинхронных вычислений. Задачи обычно менее составные, чем объекты F# Async<T> , так как они представляют "уже выполняющиеся" задачи и не могут быть составлены совместно способами, которые выполняют параллельную композицию или скрывают распространение сигналов отмены и других контекстных параметров.

Однако, несмотря на это, методы, возвращающие задачи, являются стандартным представлением асинхронного программирования в .NET.

/// A type in a component designed for use from other .NET languages
type MyType() =

    let compute (x: int): Async<int> = async { ... }

    member this.ComputeAsync(x) = compute x |> Async.StartAsTask

Вы также часто хотите принять явный маркер отмены:

/// A type in a component designed for use from other .NET languages
type MyType() =
    let compute(x: int): Async<int> = async { ... }
    member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)

Используйте типы делегатов .NET вместо типов функций F#

Ниже приведены такие типы функций F#, как int -> intстрелка.

Вместо этого:

member this.Transform(f: int->int) =
    ...

Процедура

member this.Transform(f: Func<int,int>) =
    ...

Тип функции F# отображается на class FSharpFunc<T,U> других языках .NET и менее подходит для языковых функций и инструментов, которые понимают типы делегатов. При создании метода более высокого порядка, предназначенных для платформа .NET Framework 3.5 или более поздней версии, System.FuncSystem.Action и делегаты являются правильными API для публикации, чтобы позволить разработчикам .NET использовать эти API с низким уровнем трения. (При выборе платформа .NET Framework 2.0 определяемые системой типы делегатов более ограничены; рассмотрите возможность использования предопределенных типов делегатов, таких как System.Converter<T,U> или определение определенного типа делегата.)

На боковой стороне делегаты .NET не являются естественными для библиотек с интерфейсом F#(см. следующий раздел о библиотеках F#). В результате общая стратегия реализации при разработке методов более высокого порядка для библиотек vanilla .NET заключается в создании всех реализаций с помощью типов функций F#, а затем создать общедоступный API с помощью делегатов в качестве тонкого фасада на вершине фактической реализации F#.

Используйте шаблон TryGetValue вместо возврата значений параметров F# и предпочитайте перегрузку метода для принятия значений параметра F# в качестве аргументов.

Распространенные шаблоны использования для типа параметра F# в API лучше реализованы в ванильных API .NET с помощью стандартных методов проектирования .NET. Вместо возврата значения параметра F# рекомендуется использовать тип возвращаемого значения bool и параметр out, как в шаблоне TryGetValue. Вместо того, чтобы использовать значения параметров F# в качестве параметров, рекомендуется использовать перегрузку метода или необязательные аргументы.

member this.ReturnOption() = Some 3

member this.ReturnBoolAndOut(outVal: byref<int>) =
    outVal <- 3
    true

member this.ParamOption(x: int, y: int option) =
    match y with
    | Some y2 -> x + y2
    | None -> x

member this.ParamOverload(x: int) = x

member this.ParamOverload(x: int, y: int) = x + y

Использование типов интерфейса коллекции .NET IEnumerable<T> и IDictionary<Key, Value> для параметров и возвращаемых значений

Избегайте использования конкретных типов коллекций, таких как массивы T[].NET, типы Map<Key,Value>list<T>F# и Set<T>типы конкретных коллекций .NET, такие какDictionary<Key,Value>. Рекомендации по проектированию библиотек .NET имеют хорошие рекомендации по использованию различных типов коллекций, таких как IEnumerable<T>. Некоторые виды использования массивов (T[]) допустимы в некоторых обстоятельствах на основе производительности. Обратите внимание, что seq<T> это просто псевдоним F# для IEnumerable<T>, и таким образом seq часто является подходящим типом для ВАНильного API .NET.

Вместо списков F#:

member this.PrintNames(names: string list) =
    ...

Используйте последовательности F#:

member this.PrintNames(names: seq<string>) =
    ...

Используйте тип единицы в качестве единственного входного типа метода для определения метода нулевого аргумента или в качестве единственного возвращаемого типа для определения метода void-returning

Избегайте других вариантов использования типа единицы. Это хорошо:

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

Это плохо:

member this.WrongUnit( x: unit, z: int) = ((), ())

Проверка значений NULL в границах API .NET для ванили

Код реализации F#, как правило, имеет меньше значений NULL из-за неизменяемых шаблонов конструктора и ограничений на использование null-литералы для типов F#. Другие языки .NET часто используют значение NULL в качестве значения гораздо чаще. Из-за этого код F#, предоставляющий ванильный API .NET, должен проверка параметры для null на границе API и предотвратить более глубокий поток этих значений в код реализации F#. Можно isNull использовать функцию или шаблон, соответствующую шаблону null .

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull` argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

Избегайте использования кортежей в качестве возвращаемых значений

Вместо этого предпочитайте возвращать именованный тип, содержащий статистические данные, или использовать параметры out для возврата нескольких значений. Хотя кортежи и кортежи структур существуют в .NET (включая поддержку языка C# для кортежей структур), они чаще всего не предоставляют идеальный и ожидаемый API для разработчиков .NET.

Избегайте использования карриинга параметров

Вместо этого используйте соглашения о вызовах Method(arg1,arg2,…,argN).NET.

member this.TupledArguments(str, num) = String.replicate num str

Совет. Если вы разрабатываете библиотеки для использования с любого языка .NET, то на самом деле нет замены на некоторые экспериментальные программы C# и Visual Basic, чтобы убедиться, что библиотеки "чувствуют себя правильно" на этих языках. Вы также можете использовать такие средства, как .NET Рефлексия or и обозреватель объектов Visual Studio, чтобы обеспечить, чтобы библиотеки и их документация отображались должным образом разработчикам.

Приложение

Полный пример разработки кода F# для использования другими языками .NET

Рассмотрим следующий класс :

open System

type Point1(angle,radius) =
    new() = Point1(angle=0.0, radius=0.0)
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
    member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
    static member Circle(n) =
        [ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]

Приведенный тип F# этого класса выглядит следующим образом:

type Point1 =
    new : unit -> Point1
    new : angle:double * radius:double -> Point1
    static member Circle : n:int -> Point1 list
    member Stretch : l:double -> Point1
    member Warp : f:(double -> double) -> Point1
    member Angle : double
    member Radius : double

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

// C# signature for the unadjusted Point1 class
public class Point1
{
    public Point1();

    public Point1(double angle, double radius);

    public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);

    public Point1 Stretch(double factor);

    public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

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

  • Метаданные, такие как имена аргументов, сохранены.

  • Методы F#, которые принимают два аргумента, становятся методами C#, которые принимают два аргумента.

  • Функции и списки становятся ссылками на соответствующие типы в библиотеке F#.

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

namespace SuperDuperFSharpLibrary.Types

type RadialPoint(angle:double, radius:double) =

    /// Return a point at the origin
    new() = RadialPoint(angle=0.0, radius=0.0)

    /// The angle to the point, from the x-axis
    member x.Angle = angle

    /// The distance to the point, from the origin
    member x.Radius = radius

    /// Return a new point, with radius multiplied by the given factor
    member x.Stretch(factor) =
        RadialPoint(angle=angle, radius=radius * factor)

    /// Return a new point, with angle transformed by the function
    member x.Warp(transform:Func<_,_>) =
        RadialPoint(angle=transform.Invoke angle, radius=radius)

    /// Return a sequence of points describing an approximate circle using
    /// the given count of points
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }

Приведенный тип F# кода выглядит следующим образом:

type RadialPoint =
    new : unit -> RadialPoint
    new : angle:double * radius:double -> RadialPoint
    static member Circle : count:int -> seq<RadialPoint>
    member Stretch : factor:double -> RadialPoint
    member Warp : transform:System.Func<double,double> -> RadialPoint
    member Angle : double
    member Radius : double

Подпись C# теперь выглядит следующим образом:

public class RadialPoint
{
    public RadialPoint();

    public RadialPoint(double angle, double radius);

    public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);

    public RadialPoint Stretch(double factor);

    public RadialPoint Warp(System.Func<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

Исправления, сделанные для подготовки этого типа к использованию в составе библиотеки .NET для ванили, приведены ниже.

  • Скорректировано несколько имен: Point1, n, lи f стал RadialPoint, countfactorи , соответственноtransform.

  • Используется тип возвращаемого значения seq<RadialPoint> вместо RadialPoint list изменения конструкции списка, используемой [ ... ] для построения последовательности.IEnumerable<RadialPoint>

  • Используется тип делегата .NET вместо типа System.Func функции F#.

Это делает его гораздо более хорошим для использования в коде C#.