Я пытаюсь создать библиотеку в 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-оболочки, идиоматическое для С#