Я пытаюсь создать библиотеку на 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 #
Ответы:
Даниэль уже объяснил, как определить версию написанной вами функции 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, чем композиция функций.источник
Вам нужно только обернуть значения функций (частично применяемые функции и т. Д.) С помощью
Func
илиAction
, остальные преобразуются автоматически. Например:Поэтому имеет смысл использовать
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 внутри себя, а затем вызвать егоAsync.StartAsTask
перед возвратом из общедоступного метода.Объятие
null
может быть самым большим недостатком при разработке API для использования с C #. Раньше я пробовал всевозможные уловки, чтобы не рассматривать null во внутреннем коде F #, но, в конце концов, лучше всего было пометить типы с общедоступными конструкторами[<AllowNullLiteral>]
и проверить аргументы на null. В этом плане он не хуже C #.Размеченные объединения обычно компилируются в иерархии классов, но всегда имеют относительно дружественное представление в C #. Я бы сказал, пометьте их
[<AllowNullLiteral>]
и используйте.Каррированные функции производят значения функций , которые не следует использовать.
Я обнаружил, что лучше принять null, чем полагаться на его перехват в публичном интерфейсе и игнорировать его внутри. YMMV.
Имеет смысл использовать
list
/map
/ дляset
внутренних целей . Все они могут быть представлены через общедоступный интерфейс какIEnumerable<_>
. Кроме того ,seq
,dict
иSeq.readonly
часто полезно.См. №6.
Какую стратегию вы выберете, зависит от типа и размера вашей библиотеки, но, по моему опыту, найти золотую середину между F # и C # потребовало меньше работы - в долгосрочной перспективе - чем создание отдельных API.
источник
module Foo.Bar.Zoo
то в C #, это будетclass Foo
в пространстве именFoo.Bar
. Однако, если у вас есть вложенные модули, такие какmodule Foo=... module Bar=... module Zoo==
, это сгенерирует классFoo
с внутренними классамиBar
иZoo
. ReIEnumerable
, это может быть неприемлемо, когда нам действительно нужно работать с картами и наборами. Renull
значения иAllowNullLiteral
- одна из причин, по которой мне нравится F #, состоит в том, что я устал отnull
C #, я действительно не хотел бы снова загрязнять им все!Хотя это, вероятно, было бы излишним, вы могли бы подумать о написании приложения с использованием Mono.Cecil (у него отличная поддержка в списке рассылки), которое автоматизирует преобразование на уровне IL. Например, вы реализуете свою сборку на F #, используя общедоступный API в стиле F #, тогда инструмент сгенерирует над ней дружественную к C # оболочку.
Например, в F # вы, очевидно, должны использовать
option<'T>
(None
, в частности) вместо использования,null
как в C #. Написание генератора-оболочки для этого сценария должно быть довольно простым: метод-оболочка будет вызывать исходный метод: если возвращаемое значение былоSome x
, то returnx
, в противном случае returnnull
.Вам нужно будет обработать случай, когда
T
это тип значения, то есть не допускающий значения NULL; вам придется обернуть возвращаемое значение метода-оболочкиNullable<T>
, что немного затруднит работу.Опять же, я совершенно уверен, что написание такого инструмента в вашем сценарии окупится, возможно, за исключением случаев, когда вы будете регулярно работать с этой такой библиотекой (которую можно легко использовать как с F #, так и с C #). В любом случае, я думаю, что это будет интересный эксперимент, который я мог бы даже исследовать когда-нибудь.
источник
Mono.Cecil
Означает ли использование, в основном, написание программы для загрузки сборки F #, поиска элементов, требующих сопоставления, и их генерации другой сборки с оболочками C #? Я правильно понял?Проект рекомендаций по разработке компонентов F # (август 2010 г.)
источник