Лучший подход для проектирования библиотек F # для использования как с F #, так и с С#

Я пытаюсь создать библиотеку в F #. Библиотека должна быть дружественной для использования как от F #, так и от С#.

И здесь я немного застрял. Я могу сделать это F # дружественным, или я могу сделать его С# дружественным, но проблема в том, как сделать его дружественным для обоих.

Вот пример. Представьте, что у меня есть следующая функция в F #:

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a

Это отлично можно использовать из F #:

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"

В С# функция compose имеет следующую подпись:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

Но, конечно, я не хочу FSharpFunc в подписи, вместо этого хочу Func и Action, например:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

Чтобы достичь этого, я могу создать функцию compose2 следующим образом:

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)

Теперь это вполне можно использовать в С#:

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}

Но теперь у нас есть проблема с использованием compose2 из F #! Теперь мне нужно обернуть все стандартные F # funs в Func и Action, например:

let useCompose2InFsharp() =
    let f = Func<_,_>(fun item -> item.ToString())
    let a = Action<_>(fun item -> printfn "%A" item)
    let composite2 = compose2 f a

    composite2.Invoke "foo"
    composite2.Invoke "bar"

Вопрос: Как мы можем добиться первоклассного опыта для библиотеки, написанной в F # для пользователей F # и С#?

До сих пор я не мог придумать ничего лучше, чем эти два подхода:

  • Две отдельные сборки: одна предназначена для пользователей F #, а вторая для пользователей С#.
  • Одна сборка, но разные пространства имен: одна для пользователей F #, вторая - для пользователей С#.

Для первого подхода я сделал бы что-то вроде этого:

  • Создайте проект F #, назовите его FooBarFs и скомпилируйте его в FooBarFs.dll.

    • Целевая библиотека для пользователей F #.
    • Скрыть все ненужные файлы .fsi.
  • Создайте еще один проект F #, вызовите if FooBarCs и скомпилируйте его в FooFar.dll

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

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

Вопрос: ни один из них не идеален, возможно, я пропускаю какой-то флаг компилятора /switch/attribte или какой-то трюк, и есть лучший способ сделать это?

Вопрос: кто-нибудь еще пытался добиться чего-то подобного, и если да, то как вы это сделали?

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


ИЗМЕНИТЬ 2:

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

Позвольте мне привести более конкретные примеры. Я прочитал самые превосходные F # Руководство по разработке компонентов документ (большое спасибо @gradbot за это!). Руководства в документе, если они используются, имеют адрес некоторые из проблем, но не все.

Документ разделен на две основные части: 1) рекомендации для таргетинга на пользователей F #; и 2) руководящие принципы для предназначенных для пользователей С#. Нигде он даже не пытается притвориться, что можно иметь форму подход, который точно повторяет мой вопрос: мы можем нацелиться на F #, мы можем нацелить С#, но что такое практическое решение для ориентации как?

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

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

Теперь к примерам, которые я беру прямо из F # Руководство по разработке компонентов.

  • Модули + функции (F #) vs Пространства имен + Типы + функции

    • F #: Используйте пространства имен или модули для хранения ваших типов и модулей. Идиоматическое использование заключается в размещении функций в модулях, например:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
      
    • С#: Использовать пространства имен, типы и члены в качестве основной организационной структуры для вашего (в отличие от модулей), для API-интерфейсов Vanilla.NET.

      Это несовместимо с директивой F #, и этот пример понадобится для перезаписи в соответствии с пользователями С#:

      [<AbstractClass; Sealed>]
      type Foo =
          static member bar() = ...
          static member zoo() = ...
      

      Таким образом, мы нарушаем идиоматическое использование из F #, потому что мы больше не можем использовать bar и zoo без префикса с помощью Foo.

  • Использование кортежей

    • F #: При необходимости используйте кортежи для возвращаемых значений.

    • С#: избегать использования кортежей в качестве возвращаемых значений в API-интерфейсах vanilla.NET.

  • Асинхронный

    • F #: Использовать Async для асинхронного программирования на границах API F #.

    • С#: выставлять асинхронные операции с использованием либо модели асинхронного программирования .NET(BeginFoo, EndFoo) или как методы, возвращающие задачи .NET(Task), а не как F # Async объекты.

  • Использование Option

    • F #: используйте значения параметров для возвращаемых типов вместо создания исключений (для F # файлового кода).

    • Рассмотрите возможность использования шаблона TryGetValue вместо того, чтобы возвращать значения опций F # (опция) в ванили .NET API и предпочитают перегрузку метода для принятия значений параметра F # в качестве аргументов.

  • Дискриминационные союзы

    • F #: использовать дискриминационные союзы в качестве альтернативы иерархиям классов для создания древовидных данных

    • С#: никаких конкретных рекомендаций для этого нет, но концепция дискриминированных союзов чужда С#

  • Выполненные функции

    • F #: точные функции являются идиоматическими для F #

    • С#: не использовать currying параметров в API-интерфейсах vanilla.NET.

  • Проверка нулевых значений

    • F #: это не идиоматично для F #

    • С#: рассмотрите проверку нулевых значений на границах API-интерфейса Vanilla.NET.

  • Использование типов F # list, map, set и т.д.

    • F #: идиоматично использовать их в F #

    • С#: рассмотрим использование типов интерфейса коллекции .NET IEnumerable и IDictionary для параметров и возвращаемых значений в API-интерфейсах vanilla.NET. (т.е. не использовать F # list, map, set)

  • Типы функций (очевидные)

    • F #: использование функций F # как значений является идиоматическим для F #, очевидно

    • С#: используйте типы делегатов .NET, предпочитая типы функций F # в API-интерфейсах Vanilla.NET.

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

Кстати, рекомендации также имеют частичный ответ:

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

Подводя итог.

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

В соответствии с директивой doc, кажется, что авторинг для F # сначала, а затем создание оболочкой для .NET является разумная стратегия.

Остается вопрос о практической реализации этого:

  • Отдельные сборки? или

  • Различные пространства имен?

Если моя интерпретация верна, Томас предполагает, что использование отдельных пространств имен должно быть достаточным и должно быть приемлемым решением.

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

  • FSharp.Foo.Bar → пространство имен для библиотеки с обращением к F #

  • Foo.Bar → пространство имен для .NET-оболочки, идиоматическое для С#

Ответы

Ответ 1

Даниэль уже объяснил, как определить С# -дружественную версию функции F #, которую вы написали, поэтому я добавлю некоторые комментарии более высокого уровня. Прежде всего, вы должны прочитать F # Руководство по разработке компонентов (ссылка уже указана gradbot). Это документ, в котором объясняется, как создавать библиотеки F # и .NET с помощью F #, и он должен отвечать на многие ваши вопросы.

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

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

  • Библиотека .NET предназначена для использования с любым языком .NET(включая С# и F #), и обычно это соответствует объектно-ориентированному стилю .NET. Это означает, что вы будете раскрывать большую часть функциональности в виде классов с помощью метода (а иногда и методов расширения или статических методов, но в основном код должен быть записан в OO-дизайне).

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

В некоторых случаях (т.е. при разработке числовых библиотек, которые не очень хорошо вписываются в стиль библиотеки .NET), имеет смысл создавать библиотеку, которая смешивает как F #, так и .NET в одной библиотеке. Лучший способ сделать это - иметь нормальный F # (или обычный .NET) API, а затем предоставить обертки для естественного использования в другом стиле. Обертки могут находиться в отдельном пространстве имен (например, MyLibrary.FSharp и MyLibrary).

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

Ответ 2

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

type A(arg) =
  member x.Invoke(f: Func<_,_>) = f.Invoke(arg)

let a = A(1)
a.Invoke(fun i -> i + 1)

Поэтому имеет смысл использовать Func/Action, где это применимо. Это устраняет ваши проблемы? Я думаю, что ваши предлагаемые решения слишком сложны. Вы можете написать всю свою библиотеку в F # и использовать ее без проблем с F # и С# (я делаю это все время).

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

ИЗМЕНИТЬ

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

Принимая ваши очки по очереди:

  • Модули + let привязки и не содержащие конструктор типы + статические члены кажутся точно такими же в С#, поэтому используйте модули, если сможете. Вы можете использовать CompiledNameAttribute, чтобы дать членам С# дружеские имена.

  • Возможно, я ошибаюсь, но я предполагаю, что Руководство по компонентам было написано до добавления System.Tuple в структуру. (В более ранних версиях F # определял свой собственный тип кортежа.) С тех пор становится более приемлемым использовать Tuple в открытом интерфейсе для тривиальных типов.

  • Вот где я думаю, что вы делаете вещи С#, потому что F # хорошо играет с Task, но С# не играет хорошо с Async. Вы можете использовать async внутри, а затем вызвать Async.StartAsTask, прежде чем вернуться из общедоступного метода.

  • Embrace of null может быть самым большим недостатком при разработке API для использования с С#. Раньше я пробовал всевозможные трюки, чтобы не учитывать значение null во внутреннем коде F #, но в конце было лучше всего пометить типы с помощью общих конструкторов с помощью [<AllowNullLiteral>] и проверить аргументы для null. Это не хуже С# в этом отношении.

  • Дискриминационные объединения обычно скомпилируются в иерархии классов, но всегда имеют относительно дружественное представление в С#. Я бы сказал, отметьте их [<AllowNullLiteral>] и используйте их.

  • Curried функции производят значения функций, которые не должны использоваться.

  • Я обнаружил, что было бы лучше принять нуль, чем зависеть от того, как он попадает в открытый интерфейс и игнорирует его внутри. YMMV.

  • Это имеет смысл использовать внутри list/map/set. Все они могут быть открыты через открытый интерфейс как IEnumerable<_>. Кроме того, seq, dict и Seq.readonly часто полезны.

  • См. № 6.

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

Ответ 3

Проект правил проектирования компонентов F # (Август 2010 г.)

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

  • Рекомендации по разработке "vanilla".NET-библиотек для использования на любом языке .NET.
  • Рекомендации для библиотек F # -to-F # и кода реализации F #.
  • Предложения по соглашениям о кодировании для кода реализации F #

Ответ 4

Хотя это, вероятно, было бы излишним, вы могли бы рассмотреть возможность написания приложения с помощью Mono.Cecil (у него есть огромная поддержка в списке рассылки) что автоматизировало бы преобразование на уровне IL. Например, вы реализуете свою сборку в F #, используя открытый API-интерфейс F #, тогда инструмент сгенерирует обертку с поддержкой С#.

Например, в F # вы, очевидно, используете option<'T> (None, в частности) вместо использования null, как в С#. Написание генератора обертки для этого сценария должно быть довольно простым: метод-оболочка будет ссылаться на исходный метод: если оно возвращало значение Some x, верните x, в противном случае верните null.

Вам нужно будет обрабатывать случай, когда T - тип значения, то есть не-nullable; вам придется обернуть возвращаемое значение метода обертки в Nullable<T>, что делает его немного болезненным.

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