Я намагаюся створити бібліотеку у 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 # ідіоми для імен, просторів імен тощо.
- Створіть обгортки, які делегуються до основної бібліотеки, роблячи перетворення, де це необхідно.
Я думаю, що другий підхід з просторами імен може викликати збиття з пантелику для користувачів, але тоді у вас є одна збірка.
Питання: Жодне з них не є ідеальним, можливо, я пропускаю якийсь прапор / перемикач / атрибут компілятора чи якийсь трюк, і є кращий спосіб зробити це?
Питання: хтось ще намагався досягти чогось подібного і якщо так, то як це зробили?
EDIT: для уточнення, питання стосується не лише функцій та делегатів, а загального досвіду користувача C # з бібліотекою F #. Сюди входять простори імен, конвенції імен, ідіоми тощо, які є власними для C #. В основному, користувач C # не повинен виявляти, що бібліотека є автором F #. І навпаки, користувач F # повинен відчувати, що має справу з бібліотекою C #.
EDIT 2:
З відповідей та коментарів я бачу, що моє запитання не має необхідної глибини, можливо, в основному через використання лише одного прикладу, коли виникають проблеми інтероперабельності між F # і C #, питання про значення функцій. Я думаю, що це найбільш очевидний приклад, і тому це змусило мене використовувати його для того, щоб задати питання, але тим же самим склало враження, що це єдине питання, яке мене хвилює.
Дозвольте навести конкретніші приклади. Я прочитав найпрекрасніший документ з керівництвом щодо дизайну компонентів F # (велике спасибі @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 #: Уникайте використання кортежів як зворотних значень у ванільних API .NET.
Асинхронізація
F #: Використовуйте Async для програмування асинхронізації на межах F # API.
C #: Викривайте асинхронні операції, використовуючи асинхронну модель програмування .NET (BeginFoo, EndFoo), або як методи повернення завдань .NET (Завдання), а не як об'єкти F # Async.
Використання
Option
F #: Подумайте про використання параметрів значень для типів повернення замість збільшення винятків (для коду F # -facing).
Подумайте про використання шаблону TryGetValue замість повернення значень параметра (опція) F # у ванільних API .NET і віддавайте перевагу методу перевантаження над прийняттям значень параметра F # як аргументи.
Дискримінаційні спілки
F #: Використовуйте дискриміновані об'єднання як альтернативу ієрархіям класів для створення даних, структурованих по дереву
C #: для цього немає конкретних вказівок, але концепція дискримінованих спілок є чужою для C #
Загорнуті функції
F #: криві функції ідіоматичні для F #
C #: Не використовуйте currying параметрів у ванільних .NET API.
Перевірка нульових значень
F #: це не ідіоматично для F #
C #: Розглянемо перевірку наявності нульових значень на межах ванільного .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 #.
Підсумовувати.
Є одна певна відповідь: немає жодних хитрощів компілятора, які я пропустив .
Відповідно до рекомендацій doc, здається, що створення спочатку F #, а потім створення фасадної обгортки для .NET є розумною стратегією.
Потім залишається питання щодо практичної реалізації цього:
Окремі збірки? або
Різні простори імен?
Якщо моє тлумачення правильне, Томаш пропонує, що використання окремих просторів імен повинно бути достатнім і повинно бути прийнятним рішенням.
Думаю, я погоджуся з цим, враховуючи, що вибір просторів імен такий, що він не здивує і не збиває з пантелику користувачів .NET / C #, а це означає, що простір імен для них, мабуть, повинен виглядати так, як це основний простір імен для них. Користувачі F # повинні взяти на себе тягар вибору простору імен для F #. Наприклад:
FSharp.Foo.Bar -> простір імен для F #, зверненого до бібліотеки
Foo.Bar -> простір імен для .NET обгортки, ідіоматичний для C #