Лучший подход для проектирования библиотек F # для использования как с F #, так и с С#
Я пытаюсь создать библиотеку в 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 #: Использовать Async для асинхронного программирования на границах API F #.
-
С#: выставлять асинхронные операции с использованием либо модели асинхронного программирования .NET(BeginFoo, EndFoo) или как методы, возвращающие задачи .NET(Task), а не как F # Async
объекты.
-
Использование Option
-
F #: используйте значения параметров для возвращаемых типов вместо создания исключений (для F # файлового кода).
-
Рассмотрите возможность использования шаблона TryGetValue вместо того, чтобы возвращать значения опций F # (опция) в ванили
.NET API и предпочитают перегрузку метода для принятия значений параметра F # в качестве аргументов.
-
Дискриминационные союзы
-
F #: использовать дискриминационные союзы в качестве альтернативы иерархиям классов для создания древовидных данных
-
С#: никаких конкретных рекомендаций для этого нет, но концепция дискриминированных союзов чужда С#
-
Выполненные функции
-
Проверка нулевых значений
-
Использование типов 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-оболочки, идиоматическое для С#
Ответы
Ответ 1
Даниэль уже объяснил, как определить С# -дружественную версию функции F #, которую вы написали, поэтому я добавлю некоторые комментарии более высокого уровня. Прежде всего, вы должны прочитать F # Руководство по разработке компонентов (ссылка уже указана gradbot). Это документ, в котором объясняется, как создавать библиотеки F # и .NET с помощью F #, и он должен отвечать на многие ваши вопросы.
При использовании F # в основном существуют два типа библиотек, которые вы можете написать:
-
Библиотека F # предназначена для использования только с F #, поэтому публичный интерфейс написан в функциональном стиле (с использованием типов функций F #, кортежей, дискриминационных союзов и т.д.)
-
Библиотека .NET предназначена для использования с любым языком .NET(включая С# и F #), и обычно это соответствует объектно-ориентированному стилю .NET. Это означает, что вы будете раскрывать большую часть функциональности в виде классов с помощью метода (а иногда и методов расширения или статических методов, но в основном код должен быть записан в OO-дизайне).
В вашем вопросе вы спрашиваете, как выставить состав функций в виде библиотеки .NET, но я думаю, что такие функции, как ваш compose
, являются слишком низкими уровнями с точки зрения библиотеки .NET. Вы можете разоблачить их как методы, работающие с Func
и Action
, но это, вероятно, не то, как вы вначале создавали бы обычную библиотеку .NET(возможно, вы использовали бы шаблон Builder или что-то в этом роде).
В некоторых случаях (т.е. при разработке числовых библиотек, которые не очень хорошо вписываются в стиль библиотеки .NET), имеет смысл создавать библиотеку, которая смешивает как F #, так и .NET в одной библиотеке. Лучший способ сделать это - иметь нормальный F # (или обычный .NET) API, а затем предоставить обертки для естественного использования в другом стиле. Обертки могут находиться в отдельном пространстве имен (например, MyLibrary.FSharp
и MyLibrary
).
В вашем примере вы можете оставить реализацию F # в MyLibrary.FSharp
, а затем добавить .NET(С#-дружественные) обертки (похожие на код, опубликованный Даниэлем) в пространстве имен MyLibrary
как статический метод некоторого класса. Но опять же, библиотека .NET, вероятно, будет иметь более специфический API, чем состав функций.
Ответ 2
Вам нужно только привязать значения функций (частично прикладные функции и т.д.) с помощью 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 # и С# (я делаю это все время).
Кроме того, F # более гибко, чем С#, в плане совместимости, поэтому обычно лучше следовать традиционному стилю .NET, если это вызывает беспокойство.
ИЗМЕНИТЬ
Работа, требуемая для создания двух открытых интерфейсов в отдельных пространствах имен, я думаю, оправдана только тогда, когда они являются взаимодополняющими или функциональность F # не может использоваться из С# (например, встроенные функции, зависящие от конкретных метаданных F #).
Принимая ваши очки по очереди:
-
Модули + let
привязки и не содержащие конструктор типы + статические члены кажутся точно такими же в С#, поэтому используйте модули, если сможете. Вы можете использовать CompiledNameAttribute
, чтобы дать членам С# дружеские имена.
-
Возможно, я ошибаюсь, но я предполагаю, что Руководство по компонентам было написано до добавления System.Tuple
в структуру. (В более ранних версиях F # определял свой собственный тип кортежа.) С тех пор становится более приемлемым использовать Tuple
в открытом интерфейсе для тривиальных типов.
-
Вот где я думаю, что вы делаете вещи С#, потому что F # хорошо играет с Task
, но С# не играет хорошо с Async
. Вы можете использовать async внутри, а затем вызвать Async.StartAsTask
, прежде чем вернуться из общедоступного метода.
-
Embrace of null
может быть самым большим недостатком при разработке API для использования с С#. Раньше я пробовал всевозможные трюки, чтобы не учитывать значение null во внутреннем коде F #, но в конце было лучше всего пометить типы с помощью общих конструкторов с помощью [<AllowNullLiteral>]
и проверить аргументы для null. Это не хуже С# в этом отношении.
-
Дискриминационные объединения обычно скомпилируются в иерархии классов, но всегда имеют относительно дружественное представление в С#. Я бы сказал, отметьте их [<AllowNullLiteral>]
и используйте их.
-
Curried функции производят значения функций, которые не должны использоваться.
-
Я обнаружил, что было бы лучше принять нуль, чем зависеть от того, как он попадает в открытый интерфейс и игнорирует его внутри. YMMV.
-
Это имеет смысл использовать внутри list
/map
/set
. Все они могут быть открыты через открытый интерфейс как IEnumerable<_>
. Кроме того, seq
, dict
и Seq.readonly
часто полезны.
-
См. № 6.
Какую стратегию вы берете, зависит от типа и размера вашей библиотеки, но, по моему опыту, поиск сладкого пятна между F # и С# требует меньше работы - в долгосрочной перспективе, чем создание отдельных API.
Ответ 3
Проект правил проектирования компонентов F #
(Август 2010 г.)
Обзор В этом документе рассматриваются некоторые проблемы, связанные с проектированием и кодированием F #. В частности, он охватывает:
- Рекомендации по разработке "vanilla".NET-библиотек для использования на любом языке .NET.
- Рекомендации для библиотек F # -to-F # и кода реализации F #.
- Предложения по соглашениям о кодировании для кода реализации F #
Ответ 4
Хотя это, вероятно, было бы излишним, вы могли бы рассмотреть возможность написания приложения с помощью Mono.Cecil (у него есть огромная поддержка в списке рассылки) что автоматизировало бы преобразование на уровне IL. Например, вы реализуете свою сборку в F #, используя открытый API-интерфейс F #, тогда инструмент сгенерирует обертку с поддержкой С#.
Например, в F # вы, очевидно, используете option<'T>
(None
, в частности) вместо использования null
, как в С#. Написание генератора обертки для этого сценария должно быть довольно простым: метод-оболочка будет ссылаться на исходный метод: если оно возвращало значение Some x
, верните x
, в противном случае верните null
.
Вам нужно будет обрабатывать случай, когда T
- тип значения, то есть не-nullable; вам придется обернуть возвращаемое значение метода обертки в Nullable<T>
, что делает его немного болезненным.
Опять же, я вполне уверен, что заплатить за такой инструмент можно в вашем сценарии, возможно, за исключением того, что вы будете работать над этой такой библиотекой (которую можно легко использовать с F # и С# на обоих). В любом случае, я думаю, это будет интересный эксперимент, который я когда-нибудь мог бы изучить.