Найкращий підхід до проектування бібліотек F # для використання як з F #, так і з C #


113

Я намагаюся створити бібліотеку у 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 #?

Поки що я не міг придумати нічого кращого, ніж ці два підходи:

  1. Дві окремі збірки: одна націлена на користувачів F #, а друга на користувачів C #.
  2. Одна збірка, але різні простори імен: одна для користувачів F #, а друга - для користувачів C #.

Для першого підходу я б зробив щось подібне:

  1. Створіть проект F #, назвіть його FooBarFs та компілюйте його у FooBarFs.dll.

    • Націлюйте бібліотеку виключно на користувачів F #.
    • Сховати все непотрібне з файлів .fsi.
  2. Створіть ще один проект 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 .

  1. Модулі + функції (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.

  2. Використання кортежів

    • F #: Використовуйте кортежі, коли це доречно для повернення значень.

    • C #: Уникайте використання кортежів як зворотних значень у ванільних API .NET.

  3. Асинхронізація

    • F #: Використовуйте Async для програмування асинхронізації на межах F # API.

    • C #: Викривайте асинхронні операції, використовуючи асинхронну модель програмування .NET (BeginFoo, EndFoo), або як методи повернення завдань .NET (Завдання), а не як об'єкти F # Async.

  4. Використання Option

    • F #: Подумайте про використання параметрів значень для типів повернення замість збільшення винятків (для коду F # -facing).

    • Подумайте про використання шаблону TryGetValue замість повернення значень параметра (опція) F # у ванільних API .NET і віддавайте перевагу методу перевантаження над прийняттям значень параметра F # як аргументи.

  5. Дискримінаційні спілки

    • F #: Використовуйте дискриміновані об'єднання як альтернативу ієрархіям класів для створення даних, структурованих по дереву

    • C #: для цього немає конкретних вказівок, але концепція дискримінованих спілок є чужою для C #

  6. Загорнуті функції

    • F #: криві функції ідіоматичні для F #

    • C #: Не використовуйте currying параметрів у ванільних .NET API.

  7. Перевірка нульових значень

    • F #: це не ідіоматично для F #

    • C #: Розглянемо перевірку наявності нульових значень на межах ванільного .NET API.

  8. Використання F типів # list, map, setі т.д.

    • F #: використовувати їх у F # ідіоматично

    • C #: Розглянемо використання типів інтерфейсу колекції .NET IEnumerable та IDictionary для параметрів та повернених значень у ванільних API .NET. ( Тобто не використовувати F # list, map,set )

  9. Типи функцій (очевидний)

    • 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 #



Окрім того, як обійтися першокласними функціями, які проблеми ви? F # вже проходить довгий шлях до бездоганного досвіду з інших мов.
Даніель

Re: Ваша редакція, мені все ще незрозуміло, чому ви вважаєте, що бібліотека з авторством F # виявиться нестандартною для C #. Здебільшого F # забезпечує набір функціоналів C #. Зробити природним чином бібліотеку - це здебільшого питання конвенції. Це правда незалежно від того, якою мовою ви користуєтесь. Все, що потрібно сказати, F # надає всі інструменти, необхідні для цього.
Даніель

Чесно кажучи, навіть якщо ви включаєте зразок коду, ви задаєте досить відкрите запитання. Ви запитуєте про "загальний досвід користувача C # з бібліотекою F #", але єдиний приклад, який ви даєте, - це те, що насправді не підтримується жодною необхідною мовою. Поводження з функціями як громадян першого класу є досить центральним принципом функціонального програмування, яке погано відображає більшість імперативних мов.
Оноріо Катенац

2
@KomradeP.: Стосовно вашого EDIT 2, FSharpx надає певні засоби для того, щоб зробити сумісність більш незрозумілою. Ви можете знайти підказки в цій чудовій публікації в блозі .
колодка

Відповіді:


40

Даніель вже пояснив, як визначити C # -дружню версію функції F #, яку ви написали, тому я додам кілька коментарів вищого рівня. Перш за все, слід ознайомитися з Посібниками з дизайну компонентів F # (на які вже посилається gradbot). Це документ, який пояснює, як проектувати бібліотеки F # і .NET за допомогою F #, і він повинен відповісти на багато ваших запитань.

При використанні F # в основному існує два типи бібліотек, якими ви можете писати:

  • Бібліотека F # призначена для використання лише з F #, тому її публічний інтерфейс написаний у функціональному стилі (використовуючи типи функцій F #, кортежі, дискриміновані союзи тощо)

  • .NET-бібліотека призначена для використання з будь-якої мови .NET (включаючи C # і F #), і, як правило, слідує .NET-орієнтований стиль .NET. Це означає, що більшу частину функціональності ви будете виставляти як класи з методом (а іноді і методами розширення або статичними методами, але в основному код повинен бути записаний в проекті OO).

У своєму запитанні ви запитуєте, як розкрити склад функції як бібліотеку .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, ніж склад функції.


1
В # компонентів Рекомендації по проектування F тепер розміщується тут
Jan SCHIEFER

19

Ви тільки обернути функціональні значення (частково застосовуються функції і т.д.) з 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, коли це викликає занепокоєння.

EDIT

Робота, необхідна для створення двох загальнодоступних інтерфейсів в окремих просторах імен, я думаю, гарантується лише тоді, коли вони є взаємодоповнюючими або функціональність F # не використовується з C # (наприклад, вбудовані функції, які залежать від F # -специфічних метаданих).

По черзі бали:

  1. Модуль + letприв’язки та статичні елементи без конструкції + статичні елементи виглядають абсолютно однаково в C #, тому перейдіть з модулями, якщо можете. Ви можете використовувати, CompiledNameAttributeщоб дати членам C # -дружні імена.

  2. Я можу помилятися, але я здогадуюсь, що Настанови щодо компонентів були написані до того, як System.Tupleвони були додані в рамки. (У попередніх версіях F # визначено, що це власний тип кортежу.) З тих пір стає більш прийнятним використовувати Tupleв загальнодоступному інтерфейсі для тривіальних типів.

  3. Тут я думаю, що у вас є справи C #, тому що F # добре грає, Taskале C # не грає добре Async. Ви можете використовувати async внутрішньо, а потім зателефонувати Async.StartAsTaskперед поверненням із загальнодоступного методу.

  4. Обійми nullможе бути єдиним найбільшим недоліком при розробці API для використання з C #. Раніше я пробував усілякі хитрощі, щоб уникнути врахування нуля у внутрішньому F #-коді, але, врешті-решт, найкраще було позначати типи з загальнодоступними конструкторами [<AllowNullLiteral>]та перевіряти аргументи на нуль. У цьому плані це не гірше, ніж C #.

  5. Дискримінаційні спілки, як правило, складаються в ієрархії класів, але завжди мають відносно доброзичливе представництво в C #. Я б сказав, позначте їх [<AllowNullLiteral>]і використовуйте.

  6. Закриті функції дають значення функцій , які не слід використовувати.

  7. Я виявив, що краще прийняти null, ніж залежати від того, що він потрапляє на загальнодоступний інтерфейс і внутрішньо ігнорує його. YMMV.

  8. Це робить багато сенсу використовувати list/ map/ setвсередині. Усі вони можуть бути відкриті через публічний інтерфейс як IEnumerable<_>. Крім того , seq, dictі Seq.readonlyчасто корисно.

  9. Див. №6.

Яку стратегію ви приймаєте, залежить від типу та розміру вашої бібліотеки, але, на мій досвід, пошук приємного місця між F # та C # вимагає менше роботи - у довгостроковій перспективі - ніж створення окремих API.


так, я можу бачити, що є деякі способи змусити роботу працювати, я не зовсім переконаний. Повторно модулі. Якщо у вас є module Foo.Bar.Zooв C #, це буде class Fooв просторі імен Foo.Bar. Однак якщо у вас такі вкладені модулі module Foo=... module Bar=... module Zoo==, це створить клас Fooіз внутрішніми класами Barта Zoo. Re IEnumerable, це може бути неприйнятним, коли нам справді потрібно працювати з картами та наборами. Re nullцінності і AllowNullLiteral- одна з причин, що мені подобається F #, тому що я втомився від nullC #, дійсно не хотів би знову забруднювати все!
Філіп П.

Загалом віддають перевагу просторам імен над вкладеними модулями для інтеропу. Re: null, я згоден з вами, це, безумовно, регрес, щоб це врахувати, але щось, з чим ви повинні мати справу, якщо ваш API буде використовуватися з C #.
Даніель

2

Хоча це, мабуть, буде надлишком, ви можете розглянути можливість написання програми за допомогою Mono.Cecil (у ній є дивовижна підтримка у списку розсилки), яка б автоматизувала перетворення на рівні IL. Наприклад, ви реалізуєте свою збірку в F #, використовуючи загальнодоступний API F # -style, тоді інструмент генерує обкладинку C #-Friendly над нею.

Наприклад, у F # ви, очевидно, використовуєте option<'T>( Noneконкретно) замість того, щоб використовувати nullяк у C #. Написати генератор обгортки для цього сценарію має бути досить простим: метод обгортки викликав би початковий метод: якщо його значенням було Some xповернення x, то поверніть , інакше поверніть null.

Вам потрібно буде обробити випадок, коли Tце тип значення, тобто не нульовий; вам доведеться зафіксувати повернене значення методу обгортки Nullable<T>, що робить його трохи болісним.

Знову ж таки, я впевнений, що писати такий інструмент у вашому сценарії окупиться, можливо, за винятком випадків, коли ви будете працювати над цією бібліотекою (легко використовувати з F # і C # обидва) регулярно. У будь-якому випадку, я думаю, що це був би цікавий експеримент, який я можу навіть вивчити колись.


звучить цікаво. Чи використовуючи Mono.Cecilсереднє в основному написання програми для завантаження збірки F #, знайти елементи, які потребують відображення, і випромінювати іншу збірку за допомогою обгортки C #? Чи правильно я це зробив?
Філіп П.

@Komrade P .: Ви можете зробити це теж, але це набагато складніше, тому що тоді вам доведеться коригувати всі посилання, щоб вони вказували на членів нової асамблеї. Набагато простіше, якщо ви просто додасте нових членів (обгортки) до вже складеної F #-написаної збірки.
ShdNx

2

Проект інструкцій з проектування компонентів F (серпень 2010 р.)

Огляд У цьому документі розглядаються деякі проблеми, пов'язані з дизайном та кодуванням компонентів F #. Зокрема, він охоплює:

  • Вказівки щодо створення "ванільних" .NET-бібліотек для використання з будь-якої мови .NET.
  • Вказівки щодо бібліотек F # -to-F # та коду реалізації F #.
  • Пропозиції щодо умов кодування для коду реалізації F #
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.