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




создание dll c# (3)

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

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

Вот пример. Представьте, что у меня есть следующая функция в 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"

В C # функция 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)

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

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 # и C #?

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

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

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

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

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

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

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

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

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

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

EDIT 2:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  3. асинхронный

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

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

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

    • F #: рассмотрите возможность использования значений параметров для возвращаемых типов вместо повышения исключений (для F # -facing code).

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

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

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

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

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

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

    • C #: не использовать currying параметров в vanilla .NET API.

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

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

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

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

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

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

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

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

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

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

Кстати, в руководящих принципах также есть частичный ответ:

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

Обобщить.

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

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

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

  • Раздельные сборки? или

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

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

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

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

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


Вам нужно только обернуть функциональные значения (частично прикладные функции и т. Д.) С помощью 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 # и C # (я делаю это все время).

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

РЕДАКТИРОВАТЬ

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

Принимая ваши очки в свою очередь:

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

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

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

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

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

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

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

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

  9. См. № 6.

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


Даниэль уже объяснил, как определить C # -дружественную версию функции F #, которую вы написали, поэтому я добавлю некоторые комментарии более высокого уровня. Прежде всего, вы должны ознакомиться с research.microsoft.com/en-us/um/cambridge/projects/fsharp/… (ссылка уже указана gradbot). Это документ, который объясняет, как создавать библиотеки F # и .NET с помощью F #, и он должен отвечать на многие ваши вопросы.

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

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

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

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

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

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


research.microsoft.com/en-us/um/cambridge/projects/fsharp/…

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

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






f#