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

Я пытаюсь создать библиотеку в 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 или какой-то трюк, и есть лучший способ сделать это?

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  6. Функции карри

    • F#: карри функции идиоматичны для F#

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

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы подвести итог.

Есть один определенный ответ: я не пропустил ни одного трюка с компилятором.

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

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

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

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

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

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

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

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

4 ответа

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

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

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

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

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

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

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

Вам нужно только обернуть значения функций (частично примененные функции и т. Д.) 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 Привязки и члены типа "без конструктора" и статические члены выглядят одинаково в C#, так что, если можете, используйте модули. Ты можешь использовать CompiledNameAttribute дать членам C# -дружественные имена.

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

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

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

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

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

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

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

  9. Смотрите № 6.

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

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

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

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

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

Проект Руководства по проектированию компонентов F# (август 2010 г.)

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

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