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

113

Я пытаюсь создать библиотеку на 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);
}

Но теперь у нас проблема с использованием compose2F #! Теперь мне нужно обернуть весь стандартный 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 #, вызовите FooBarCs и скомпилируйте его в FooFar.dll

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

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

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

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

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


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

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

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

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

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

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

Теперь перейдем к примерам, которые я взял прямо из F # Component Design Guidelines .

  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 #: избегайте использования кортежей в качестве возвращаемых значений в обычных .NET API.

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

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

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

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

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

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

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

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

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

  6. Карри функции

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

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

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

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

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

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

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

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

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

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

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

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

Кстати, в гайдлайнах тоже есть частичный ответ:

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

Чтобы обобщить.

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

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

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

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

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

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

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

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

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

Филип П.
источник
8
В некоторой степени актуальное исследование.
Microsoft.com/en-us/um/cambridge/projects/fsharp/…
Что вас беспокоит, кроме передачи первоклассных функций? F # уже прошел долгий путь к обеспечению беспрепятственного взаимодействия с другими языками.
Даниэль
Re: ваше редактирование, мне до сих пор неясно, почему вы думаете, что библиотека, созданная на F #, будет нестандартной для C #. По большей части F # предоставляет расширенный набор функций C #. Сделать библиотеку естественной - это в основном вопрос соглашения, которое верно независимо от того, какой язык вы используете. В общем, F # предоставляет все необходимые инструменты для этого.
Дэниел
Честно говоря, даже если вы включили образец кода, вы задаете довольно открытый вопрос. Вы спрашиваете об «общем опыте пользователя C # с библиотекой F #», но единственный пример, который вы приводите, - это то, что действительно плохо поддерживается ни в одном императивным языком. Отношение к функциям как к гражданам первого класса - довольно центральный принцип функционального программирования, который плохо согласуется с большинством императивных языков.
Онорио Катеначчи
2
@KomradeP .: что касается вашего EDIT 2, FSharpx предоставляет некоторые средства, чтобы сделать совместимость более простой. Вы можете найти несколько советов в этом отличном сообщении в блоге .
колодка

Ответы:

40

Даниэль уже объяснил, как определить версию написанной вами функции F # для C #, поэтому я добавлю несколько комментариев более высокого уровня. Прежде всего, вы должны прочитать Руководство по разработке компонентов 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 #) оболочки (аналогичные коду, который опубликовал Дэниел) в MyLibraryпространстве имен как статический метод некоторого класса. Но опять же, библиотека .NET, вероятно, будет иметь более конкретный API, чем композиция функций.

Томаш Петричек
источник
1
Руководство по проектированию компонентов F # теперь размещено здесь
Ян Шифер,
19

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

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

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

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

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

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

  9. См. №6.

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

Даниэль
источник
да, я вижу, что есть несколько способов заставить все работать, я не совсем уверен. Re modules. Если у вас есть module Foo.Bar.Zooто в C #, это будет class Fooв пространстве имен Foo.Bar. Однако, если у вас есть вложенные модули, такие как module Foo=... module Bar=... module Zoo==, это сгенерирует класс Fooс внутренними классами Barи Zoo. Re IEnumerable, это может быть неприемлемо, когда нам действительно нужно работать с картами и наборами. Re nullзначения и AllowNullLiteral- одна из причин, по которой мне нравится F #, состоит в том, что я устал от nullC #, я действительно не хотел бы снова загрязнять им все!
Филип П.
Обычно для взаимодействия предпочитают пространства имен вложенным модулям. Re: null, я согласен с вами, это, конечно, регресс, чтобы его учитывать, но то, с чем вам придется иметь дело, если ваш API будет использоваться из C #.
Даниэль
2

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

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

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

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

ShdNx
источник
звучит интересно. Mono.CecilОзначает ли использование, в основном, написание программы для загрузки сборки F #, поиска элементов, требующих сопоставления, и их генерации другой сборки с оболочками C #? Я правильно понял?
Филип П.
@Komrade P .: Вы тоже можете это сделать, но это намного сложнее, потому что тогда вам придется настроить все ссылки, чтобы они указывали на элементы новой сборки. Это намного проще, если вы просто добавите новые элементы (оболочки) к существующей сборке, написанной на F #.
ShdNx
2

Проект рекомендаций по разработке компонентов F # (август 2010 г.)

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

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