Рекомендации по проектированию компонентов 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 и краткие сведения при использовании этих типов и членов, а также включить создание файлов документации для библиотеки. См. 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 #.

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

Избегайте сокращений

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

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

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

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

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

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

Используйте PascalCase для универсальных имен параметров в общедоступных API, включая библиотеки для F #. В частности, используйте такие имена T , как,,, U T1 T2 для произвольных универсальных параметров, и если конкретные имена имеют смысл, то для библиотек, предназначенных для F #, используйте такие имена Key , как, Value , Arg (но не например, TKey ).

Использование PascalCase или camelCase для открытых функций и значений в модулях F

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

Разработка объектов, типов и модулей

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

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

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

или

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, которые можно использовать для достижения того, что операторов в обычном режиме. Кроме того, они могут использоваться для кодирования типов существующий в программу, а записи функций — нет.

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

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

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

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

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

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

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

Рассмотрите возможность использования Рекуирекуалифиедакцесс и тщательного применения атрибутов Автооткрытия

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

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

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

Например, Библиотека статистики масшеавен. 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.

Рассмотрите возможность использования CompiledName для предоставления. 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 # в-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 # в-F #. Это обусловлено тем, что структуры библиотек на основе незнакомых или нестандартных неявных ограничений обычно приводят к тому, что пользовательский код становится негибким и привязан к одному конкретному шаблону платформы.

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

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

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

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

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

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

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

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

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

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

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

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

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

Например, можно определить несколько карт как особый случай для схемы F #, например:

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

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

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

При проектировании библиотек для использования из других языков .NET важно соблюдать рекомендации по проектированию библиотеки .NET. В этом документе эти библиотеки помечены как библиотеки обычный .NET, а не библиотеки F #, использующие конструкции F # без ограничений. разработка библиотек обычный .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 # в-F #. Они являются отличным устройством реализации при внутреннем использовании в компонентах и библиотеках.

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

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

Кроме того, можно дополнить типы, использующие представление UNION внутри с элементами для предоставления требуемого значения. Интерфейс 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 (вместо Event , который просто использует FSharpHandler тип по умолчанию), чтобы события публиковались в привычном виде для других языков .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.Func System.Action делегаты и являются верными api-интерфейсами для публикации, чтобы позволить разработчикам .net использовать эти api с низким уровнем трения. (при нацеливании на платформа .NET Framework 2,0, определяемые системой типы делегатов более ограничены; рассмотрите возможность использования предопределенных типов делегатов, например, System.Converter<T,U> или определения конкретного типа делегата.)

На стороне перелистывания делегаты .NET не являются естественными для библиотек, предназначенных для F # (см. следующий раздел о библиотеках, доступных в F #). В результате распространенной стратегией реализации при разработке методов более высокого порядка для библиотек обычный .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> для параметров и возвращаемых значений

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

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

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

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

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

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

Избегайте других применений типа единиц измерения. Это хорошо:

✔ 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 ()

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

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

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

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

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

совет. если вы разрабатываете библиотеки для использования на любом языке .net, нет никакой замены на фактическое выполнение экспериментального программирования на C# и Visual Basic, чтобы гарантировать правильное использование библиотек на этих языках. кроме того, можно использовать такие средства, как .net reflector и обозреватель объектов 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 , count , factor и transform соответственно.

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

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

Это позволяет лучше использовать код C#.