Лучший подход для разработки библиотек 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#?
До сих пор я не мог придумать ничего лучше, чем эти два подхода:
- Две отдельные сборки: одна предназначена для пользователей F#, а вторая для пользователей C#.
- Одна сборка, но разные пространства имен: одна для пользователей F#, а вторая для пользователей C#.
Для первого подхода я бы сделал что-то вроде этого:
Создайте проект F#, назовите его FooBarFs и скомпилируйте его в FooBarFs.dll.
- Ориентируйте библиотеку исключительно на пользователей F#.
- Скрыть все ненужное из файлов.fsi.
Создайте другой проект 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#.
Модули + функции (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#: Избегайте использования кортежей в качестве возвращаемых значений в vanilla .NET API.
асинхронный
F#: используйте Async для асинхронного программирования на границах F# API.
C#: Предоставляйте асинхронные операции, используя либо модель асинхронного программирования.NET (BeginFoo, EndFoo), либо как методы, возвращающие задачи.NET (Task), а не как объекты F# Async.
Использование
Option
F#: рассмотрите возможность использования значений опций для типов возвращаемых данных вместо создания исключений (для кода F#).
Попробуйте использовать шаблон TryGetValue вместо возврата значений опции F# (option) в API-интерфейсах vanilla.NET и предпочитайте перегрузку метода приему значений параметра F# в качестве аргументов.
Дискриминационные союзы
F#: Использовать дискриминационные объединения в качестве альтернативы иерархии классов для создания древовидных данных
C#: нет конкретных указаний для этого, но концепция дискриминируемых союзов чужда C#
Функции карри
F#: карри функции идиоматичны для F#
C#: не используйте каррирование параметров в vanilla .NET API.
Проверка на нулевые значения
F#: это не идиоматично для F#
C#: рассмотрите возможность проверки нулевых значений на границах vanilla .NET API.
Использование типов F#
list
,map
,set
, так далееF#: идиоматично использовать их в F#
C#: рассмотрите возможность использования типов интерфейса коллекции.NET IEnumerable и IDictionary для параметров и возвращаемых значений в стандартных API.NET. (т.е. не используйте F#
list
,map
,set
)
Типы функций (очевидный)
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#).
Принимая ваши очки по очереди:
Модуль +
let
Привязки и члены типа "без конструктора" и статические члены выглядят одинаково в C#, так что, если можете, используйте модули. Ты можешь использоватьCompiledNameAttribute
дать членам C# -дружественные имена.Я могу ошибаться, но я предполагаю, что Руководство по компонентам было написано до
System.Tuple
будучи добавленным к структуре. (В более ранних версиях F# определял свой собственный тип кортежа.) С тех пор он стал более приемлемым для использованияTuple
в публичном интерфейсе для тривиальных типов.Здесь я думаю, что вы должны делать вещи C#, потому что F# хорошо играет с
Task
но C# не очень хорошоAsync
, Вы можете использовать асинхронно внутри, а затем позвонитьAsync.StartAsTask
перед возвращением из публичного метода.Объятие
null
может быть единственным большим недостатком при разработке API для использования на C#. В прошлом я пробовал все виды трюков, чтобы не учитывать нуль во внутреннем коде F#, но, в конце концов, было лучше пометить типы с помощью открытых конструкторов с помощью[<AllowNullLiteral>]
и проверьте аргументы для нуля. В этом отношении он не хуже C#.Дискриминационные объединения обычно компилируются в иерархии классов, но всегда имеют относительно дружественное представление в C#. Я бы сказал, пометить их
[<AllowNullLiteral>]
и использовать их.Каррированные функции создают значения функций, которые не должны использоваться.
Я обнаружил, что лучше принять ноль, чем зависеть от того, будет ли он пойман на общедоступном интерфейсе, и игнорировать его внутренне. YMMV.
Это имеет большой смысл использовать
list
/map
/set
внутренне. Все они могут быть выставлены через открытый интерфейс какIEnumerable<_>
, Также,seq
,dict
, а такжеSeq.readonly
часто полезны.Смотрите № 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#