Я пытаюсь создать библиотеку на 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 #?
Пока что я не мог придумать ничего лучше этих двух подходов:
- Две отдельные сборки: одна предназначена для пользователей F #, а вторая - для пользователей C #.
- Одна сборка, но разные пространства имен: одно для пользователей F #, а второе для пользователей C #.
Для первого подхода я бы сделал что-то вроде этого:
Создайте проект F #, назовите его FooBarFs и скомпилируйте его в FooBarFs.dll.
- Ориентируйте библиотеку исключительно на пользователей F #.
- Скройте все лишнее из файлов .fsi.
Создайте еще один проект 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 .
Модули + функции (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
.
Использование кортежей
F #: используйте кортежи, когда это необходимо для возвращаемых значений.
C #: избегайте использования кортежей в качестве возвращаемых значений в обычных .NET API.
Асинхронный
F #: используйте Async для асинхронного программирования на границах F # API.
C #: предоставлять асинхронные операции с использованием модели асинхронного программирования .NET (BeginFoo, EndFoo) или в качестве методов, возвращающих задачи .NET (Task), а не как объекты F # Async.
Использование
Option
F #: рассмотрите возможность использования значений параметров для возвращаемых типов вместо создания исключений (для кода, обращенного к F #).
Рассмотрите возможность использования шаблона TryGetValue вместо того, чтобы возвращать значения параметра F # (option) в обычных .NET API, и предпочтите перегрузку метода, а не принятие значений параметров F # в качестве аргументов.
Дискриминационные союзы
F #: используйте размеченные объединения в качестве альтернативы иерархиям классов для создания данных с древовидной структурой
C #: никаких конкретных указаний для этого нет, но концепция дискриминируемых объединений чужда C #
Карри функции
F #: каррированные функции идиоматичны для F #
C #: не используйте каррирование параметров в обычных .NET API.
Проверка нулевых значений
F #: это не идиоматика для F #
C #: подумайте о проверке нулевых значений на границах стандартного .NET API.
Использование F типов #
list
,map
,set
и т.д.F #: идиоматично использовать их в F #
C #: рассмотрите возможность использования типов интерфейса коллекции .NET IEnumerable и IDictionary для параметров и возвращаемых значений в обычных .NET API. ( Т.е. не использовать F #
list
,map
,set
)
Типы функций (очевидный)
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 #