Pendekatan terbaik untuk mendesain perpustakaan F # untuk digunakan dari F # dan C #

113

Saya mencoba mendesain perpustakaan di F #. Perpustakaan harus ramah untuk digunakan dari F # dan C # .

Dan di sinilah saya sedikit terjebak. Saya bisa membuatnya ramah F #, atau saya bisa membuatnya bersahabat C #, tapi masalahnya adalah bagaimana membuatnya bersahabat untuk keduanya.

Berikut ini contohnya. Bayangkan saya memiliki fungsi berikut di F #:

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a

Ini sangat dapat digunakan dari F #:

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"

Di C #, composefungsi tersebut memiliki tanda tangan berikut:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

Tapi tentu saja, saya tidak ingin FSharpFuncdi tanda tangan, apa yang saya inginkan adalah Funcdan Actionsebaliknya, seperti ini:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

Untuk mencapai ini, saya dapat membuat compose2fungsi seperti ini:

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)

Sekarang, ini dapat digunakan dengan sempurna di C #:

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}

Tapi sekarang kami memiliki masalah menggunakan compose2dari F #! Sekarang saya harus membungkus semua F # standar funske dalam Funcdan Action, seperti ini:

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"

Pertanyaan: Bagaimana kita bisa mencapai pengalaman kelas satu untuk perpustakaan yang ditulis dalam F # untuk pengguna F # dan C #?

Sejauh ini, saya tidak dapat menemukan yang lebih baik dari dua pendekatan ini:

  1. Dua rakitan terpisah: satu ditargetkan untuk pengguna F #, dan yang kedua untuk pengguna C #.
  2. Satu rakitan tetapi ruang nama berbeda: satu untuk pengguna F #, dan yang kedua untuk pengguna C #.

Untuk pendekatan pertama, saya akan melakukan sesuatu seperti ini:

  1. Buat proyek F #, beri nama FooBarFs dan kompilasi ke FooBarFs.dll.

    • Targetkan perpustakaan hanya untuk pengguna F #.
    • Sembunyikan semua yang tidak perlu dari file .fsi.
  2. Buat proyek F # lain, panggil jika FooBarCs dan kompilasi ke FooFar.dll

    • Gunakan kembali proyek F # pertama di tingkat sumber.
    • Buat file .fsi yang menyembunyikan segala sesuatu dari proyek itu.
    • Buat file .fsi yang mengekspos perpustakaan dengan cara C #, menggunakan idiom C # untuk nama, ruang nama, dll.
    • Buat pembungkus yang mendelegasikan ke pustaka inti, melakukan konversi jika perlu.

Saya pikir pendekatan kedua dengan ruang nama dapat membingungkan pengguna, tetapi kemudian Anda memiliki satu rakitan.

Pertanyaannya: Tak satu pun dari ini yang ideal, mungkin saya kehilangan semacam flag / switch / atribut kompiler atau semacam trik dan ada cara yang lebih baik untuk melakukan ini?

Pertanyaannya: adakah orang lain yang mencoba mencapai sesuatu yang serupa dan jika demikian, bagaimana Anda melakukannya?

EDIT: untuk memperjelas, pertanyaannya bukan hanya tentang fungsi dan delegasi tetapi pengalaman keseluruhan pengguna C # dengan pustaka F #. Ini termasuk ruang nama, konvensi penamaan, idiom dan sejenisnya yang berasal dari C #. Pada dasarnya, pengguna C # seharusnya tidak dapat mendeteksi bahwa pustaka dibuat dalam F #. Dan sebaliknya, pengguna F # harus merasa ingin berurusan dengan pustaka C #.


EDIT 2:

Saya dapat melihat dari jawaban dan komentar sejauh ini bahwa pertanyaan saya kurang mendalam, mungkin sebagian besar karena hanya menggunakan satu contoh di mana masalah interoperabilitas antara F # dan C # muncul, masalah nilai fungsi. Saya pikir ini adalah contoh yang paling jelas dan ini membuat saya menggunakannya untuk mengajukan pertanyaan, tetapi dengan cara yang sama memberi kesan bahwa ini adalah satu-satunya masalah yang saya khawatirkan.

Izinkan saya memberikan contoh yang lebih konkret. Saya telah membaca dokumen Panduan Desain Komponen F # yang paling bagus (terima kasih banyak @gradbot untuk ini!). Pedoman dalam dokumen, jika digunakan, menangani beberapa masalah tetapi tidak semua.

Dokumen ini dibagi menjadi dua bagian utama: 1) pedoman untuk menargetkan pengguna F #; dan 2) pedoman untuk menargetkan pengguna C #. Tidak ada tempat bahkan mencoba untuk berpura-pura bahwa mungkin untuk memiliki pendekatan seragam, yang persis menggemakan pertanyaan saya: kita dapat menargetkan F #, kita dapat menargetkan C #, tetapi apa solusi praktis untuk menargetkan keduanya?

Untuk mengingatkan, tujuannya adalah memiliki perpustakaan yang dibuat dalam F #, dan yang dapat digunakan secara idiomatis dari kedua bahasa F # dan C #.

Kata kuncinya di sini adalah idiomatik . Masalahnya bukanlah interoperabilitas umum yang hanya memungkinkan untuk menggunakan perpustakaan dalam berbagai bahasa.

Sekarang untuk contoh-contoh yang saya ambil langsung dari F # Component Design Guidelines .

  1. Modul + fungsi (F #) vs Namespaces + Jenis + fungsi

    • F #: Gunakan ruang nama atau modul untuk menampung tipe dan modul Anda. Penggunaan idiomatik adalah untuk menempatkan fungsi dalam modul, misalnya:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
    • C #: Gunakan ruang nama, jenis dan anggota sebagai struktur organisasi utama untuk komponen Anda (sebagai lawan dari modul), untuk vanilla .NET API.

      Ini tidak sesuai dengan pedoman F #, dan contohnya perlu ditulis ulang agar sesuai dengan pengguna C #:

      [<AbstractClass; Sealed>]
      type Foo =
          static member bar() = ...
          static member zoo() = ...

      Dengan melakukan itu, kami menghentikan penggunaan idiomatik dari F # karena kami tidak dapat lagi menggunakan bardan zootanpa mengawalnya Foo.

  2. Penggunaan tupel

    • F #: Gunakan tupel jika sesuai untuk nilai yang dikembalikan.

    • C #: Hindari menggunakan tupel sebagai nilai kembalian di vanilla .NET API.

  3. Asinkron

    • F #: Gunakan Async untuk pemrograman async pada batas F # API.

    • C #: Lakukan operasi asinkron menggunakan baik model pemrograman asinkron .NET (BeginFoo, EndFoo), atau sebagai metode yang mengembalikan tugas .NET (Tugas), bukan sebagai objek Async F #.

  4. Penggunaan Option

    • F #: Pertimbangkan menggunakan nilai opsi untuk tipe kembalian daripada memunculkan pengecualian (untuk kode penghubung F #).

    • Pertimbangkan untuk menggunakan pola TryGetValue daripada mengembalikan nilai opsi F # (opsi) di vanilla .NET API, dan lebih suka metode overloading daripada mengambil nilai opsi F # sebagai argumen.

  5. Serikat pekerja yang terdiskriminasi

    • F #: Gunakan serikat terdiskriminasi sebagai alternatif hierarki kelas untuk membuat data terstruktur pohon

    • C #: tidak ada pedoman khusus untuk ini, tetapi konsep serikat yang terdiskriminasi asing bagi C #

  6. Fungsi kari

    • F #: fungsi kari idiomatik untuk F #

    • C #: Jangan gunakan kari parameter di vanilla .NET API.

  7. Memeriksa nilai nol

    • F #: ini tidak idiomatis untuk F #

    • C #: Pertimbangkan untuk memeriksa nilai null pada batas vanilla .NET API.

  8. Penggunaan F jenis # list, map, set, dll

    • F #: Ini adalah idiomatik untuk menggunakan ini di F #

    • C #: Pertimbangkan untuk menggunakan jenis antarmuka koleksi .NET IEnumerable dan IDictionary untuk parameter dan nilai kembalian di vanilla .NET API. ( Yaitu tidak menggunakan F # list, map,set )

  9. Jenis fungsi (yang jelas)

    • F #: penggunaan fungsi F # sebagai nilai adalah idiomatik untuk F #, jelas

    • C #: Gunakan jenis delegasi .NET dalam preferensi untuk jenis fungsi F # di vanilla .NET API.

Saya pikir ini seharusnya cukup untuk menunjukkan sifat pertanyaan saya.

Kebetulan, pedoman tersebut juga memiliki jawaban parsial:

... strategi implementasi umum saat mengembangkan metode tingkat tinggi untuk pustaka .NET vanilla adalah membuat semua implementasi menggunakan jenis fungsi F #, dan kemudian membuat API publik menggunakan delegasi sebagai façade tipis di atas implementasi F # yang sebenarnya.

Untuk meringkas.

Ada satu jawaban pasti: tidak ada trik compiler yang saya lewatkan .

Sesuai dengan dokumen pedoman, tampaknya membuat F # terlebih dahulu dan kemudian membuat pembungkus fasad untuk .NET adalah strategi yang masuk akal.

Pertanyaan selanjutnya mengenai implementasi praktis dari ini:

  • Majelis terpisah? atau

  • Ruang nama berbeda?

Jika interpretasi saya benar, Tomas menyarankan bahwa menggunakan ruang nama terpisah sudah cukup, dan harus menjadi solusi yang dapat diterima.

Saya pikir saya akan setuju dengan itu mengingat bahwa pilihan namespace sedemikian rupa sehingga tidak mengejutkan atau membingungkan pengguna .NET / C #, yang berarti bahwa namespace untuk mereka mungkin harus terlihat seperti itu adalah namespace utama untuk mereka. Pengguna F # harus memikul beban untuk memilih namespace khusus F #. Sebagai contoh:

  • FSharp.Foo.Bar -> namespace untuk F # menghadap perpustakaan

  • Foo.Bar -> namespace untuk .NET wrapper, idiomatis untuk C #

Philip P.
sumber
8
Riset yang
gradbot
Selain membagikan fungsi kelas satu, apa kekhawatiran Anda? F # telah memberikan pengalaman yang mulus dari bahasa lain.
Daniel
Re: suntingan Anda, saya masih tidak jelas mengapa menurut Anda perpustakaan yang ditulis dalam F # akan tampak non-standar dari C #. Untuk sebagian besar, F # menyediakan superset dari fungsionalitas C #. Membuat perpustakaan terasa alami sebagian besar adalah masalah kesepakatan, yang benar tidak peduli bahasa apa yang Anda gunakan. Semua itu mengatakan, F # menyediakan semua alat yang Anda butuhkan untuk melakukan ini.
Daniel
Sejujurnya, meskipun Anda menyertakan contoh kode, Anda mengajukan pertanyaan yang cukup terbuka. Anda bertanya tentang "keseluruhan pengalaman pengguna C # dengan pustaka F #" tetapi satu-satunya contoh yang Anda berikan adalah sesuatu yang sebenarnya tidak didukung dengan baik dalam bahasa imperatif apa pun . Memperlakukan fungsi sebagai warga negara kelas satu adalah prinsip yang cukup sentral dari pemrograman fungsional - yang memetakan dengan buruk ke sebagian besar bahasa penting.
Onorio Catenacci
2
@KomradeP .: terkait EDIT 2 Anda, FSharpx menyediakan beberapa cara untuk membuat interoperabilitas lebih tampak tidak terlihat. Anda bisa menemukan beberapa petunjuk di posting blog yang luar biasa ini .
pad

Jawaban:

40

Daniel sudah menjelaskan cara mendefinisikan versi C # -friendly dari fungsi F # yang Anda tulis, jadi saya akan menambahkan beberapa komentar tingkat tinggi. Pertama-tama, Anda harus membaca Panduan Desain Komponen F # (sudah direferensikan oleh gradbot). Ini adalah dokumen yang menjelaskan cara merancang pustaka F # dan .NET menggunakan F # dan seharusnya menjawab banyak pertanyaan Anda.

Saat menggunakan F #, pada dasarnya ada dua jenis pustaka yang dapat Anda tulis:

  • Perpustakaan F # dirancang untuk digunakan hanya dari F #, jadi antarmuka publiknya ditulis dalam gaya fungsional (menggunakan jenis fungsi F #, tupel, serikat yang terdiskriminasi, dll.)

  • Pustaka .NET dirancang untuk digunakan dari bahasa .NET apa pun (termasuk C # dan F #) dan biasanya mengikuti gaya berorientasi objek .NET. Ini berarti Anda akan mengekspos sebagian besar fungsionalitas sebagai kelas dengan metode (dan terkadang metode ekstensi atau metode statis, tetapi sebagian besar kode harus ditulis dalam desain OO).

Dalam pertanyaan Anda, Anda bertanya bagaimana cara mengekspos komposisi fungsi sebagai pustaka .NET, tapi menurut saya fungsi seperti Anda composeadalah konsep tingkat yang terlalu rendah dari sudut pandang pustaka .NET. Anda dapat mengeksposnya sebagai metode yang bekerja dengan Funcdan Action, tetapi itu mungkin bukan cara Anda mendesain pustaka .NET normal di tempat pertama (mungkin Anda akan menggunakan pola Builder sebagai gantinya atau sesuatu seperti itu).

Dalam beberapa kasus (yaitu saat mendesain pustaka numerik yang tidak benar-benar cocok dengan gaya pustaka .NET), masuk akal untuk merancang pustaka yang menggabungkan gaya F # dan .NET dalam satu pustaka. Cara terbaik untuk melakukannya adalah dengan memiliki F # (atau normal .NET) API dan kemudian menyediakan pembungkus untuk penggunaan alami dalam gaya lain. Wrappers bisa berada di namespace terpisah (seperti MyLibrary.FSharpdan MyLibrary).

Dalam contoh Anda, Anda dapat meninggalkan implementasi F # MyLibrary.FSharpdan kemudian menambahkan pembungkus .NET (C # -friendly) (mirip dengan kode yang diposting Daniel) di MyLibrarynamespace sebagai metode statis dari beberapa kelas. Tapi sekali lagi, pustaka .NET mungkin akan memiliki API yang lebih spesifik daripada komposisi fungsi.

Tomas Petricek
sumber
1
Panduan Desain Komponen F # sekarang dihosting di sini
Jan Schiefer
19

Anda hanya perlu membungkus nilai fungsi (fungsi yang diterapkan sebagian, dll) dengan Funcatau Action, sisanya akan dikonversi secara otomatis. Sebagai contoh:

type A(arg) =
  member x.Invoke(f: Func<_,_>) = f.Invoke(arg)

let a = A(1)
a.Invoke(fun i -> i + 1)

Jadi masuk akal untuk digunakan Func/ Actionjika memungkinkan. Apakah ini menghilangkan kekhawatiran Anda? Saya pikir solusi yang Anda usulkan terlalu rumit. Anda dapat menulis seluruh perpustakaan Anda di F # dan menggunakannya tanpa rasa sakit dari F # dan C # (Saya melakukannya sepanjang waktu).

Selain itu, F # lebih fleksibel daripada C # dalam hal interoperabilitas sehingga umumnya yang terbaik adalah mengikuti gaya NET tradisional saat ini menjadi perhatian.

EDIT

Pekerjaan yang diperlukan untuk membuat dua antarmuka publik dalam namespace terpisah, menurut saya, hanya dijamin jika keduanya saling melengkapi atau fungsionalitas F # tidak dapat digunakan dari C # (seperti fungsi inline, yang bergantung pada F #-metadata spesifik).

Mengambil poin Anda secara bergantian:

  1. letBinding modul + dan tipe tanpa konstruktor + anggota statis tampak persis sama di C #, jadi gunakan modul jika Anda bisa. Anda dapat menggunakan CompiledNameAttributeuntuk memberi anggota C # nama yang ramah.

  2. Saya mungkin salah, tapi dugaan saya adalah bahwa Panduan Komponen ditulis sebelum System.Tupleditambahkan ke kerangka kerja. (Dalam versi sebelumnya F # mendefinisikan tipe tupelnya sendiri.) Sejak itu menjadi lebih dapat diterima untuk digunakan Tupledalam antarmuka publik untuk tipe sepele.

  3. Di sinilah saya pikir Anda harus melakukan hal-hal dengan cara C # karena F # bermain dengan baik Tasktetapi C # tidak bermain dengan baik Async. Anda dapat menggunakan async secara internal kemudian memanggil Async.StartAsTasksebelum kembali dari metode publik.

  4. Embrace of nullmungkin merupakan satu-satunya kelemahan terbesar saat mengembangkan API untuk digunakan dari C #. Di masa lalu, saya mencoba semua jenis trik untuk menghindari mempertimbangkan null dalam kode F # internal tetapi, pada akhirnya, yang terbaik adalah menandai tipe dengan konstruktor publik dengan [<AllowNullLiteral>]dan memeriksa argumen untuk null. Ini tidak lebih buruk dari C # dalam hal ini.

  5. Serikat pekerja terdiskriminasi umumnya dikompilasi ke hierarki kelas tetapi selalu memiliki representasi yang relatif ramah di C #. Saya akan berkata, tandai dengan [<AllowNullLiteral>]dan gunakan.

  6. Fungsi curried menghasilkan nilai fungsi , yang tidak boleh digunakan.

  7. Saya merasa lebih baik merangkul null daripada bergantung padanya tertangkap di antarmuka publik dan mengabaikannya secara internal. YMMV.

  8. Sangat masuk akal untuk menggunakan list/ map/ secara setinternal. Mereka semua dapat diekspos melalui antarmuka publik sebagai IEnumerable<_>. Juga, seq, dict, dan Seq.readonlysering berguna.

  9. Lihat # 6.

Strategi mana yang Anda ambil bergantung pada jenis dan ukuran library Anda, tetapi, menurut pengalaman saya, menemukan sweet spot antara F # dan C # membutuhkan lebih sedikit pekerjaan — dalam jangka panjang — daripada membuat API terpisah.

Daniel
sumber
ya, saya dapat melihat ada beberapa cara untuk membuat segala sesuatunya bekerja, saya tidak sepenuhnya yakin. Modul ulang. Jika Anda memilikinya module Foo.Bar.Zoodi C # ini akan berada class Foodi namespace Foo.Bar. Namun jika Anda memiliki modul bersarang seperti module Foo=... module Bar=... module Zoo==ini, ini akan menghasilkan kelas Foodengan kelas dalam Bardan Zoo. Re IEnumerable, ini mungkin tidak dapat diterima ketika kita benar-benar perlu bekerja dengan peta dan set. Re nullmenghargai dan AllowNullLiteral- salah satu alasan saya suka F # adalah karena saya bosan nulldengan C #, benar-benar tidak ingin mencemari semuanya dengan itu lagi!
Philip P.
Umumnya lebih menyukai ruang nama daripada modul bersarang untuk interop. Re: null, saya setuju dengan Anda, ini pasti regresi untuk mempertimbangkannya, tetapi sesuatu yang harus Anda tangani jika API Anda akan digunakan dari C #.
Daniel
2

Meskipun mungkin akan berlebihan, Anda dapat mempertimbangkan untuk menulis aplikasi menggunakan Mono.Cecil (memiliki dukungan luar biasa di milis) yang akan mengotomatiskan konversi pada tingkat IL. Misalnya, Anda mengimplementasikan perakitan Anda di F #, menggunakan API publik bergaya F #, maka alat tersebut akan menghasilkan pembungkus ramah-C # di atasnya.

Misalnya, di F # Anda jelas akan menggunakan option<'T>( None, secara khusus) daripada menggunakan nullseperti di C #. Menulis generator pembungkus untuk skenario ini seharusnya cukup mudah: metode pembungkus akan memanggil metode asli: jika nilai kembaliannya adalah Some x, maka kembalikan x, atau kembalikan null.

Anda akan perlu menangani kasus ketika Tmerupakan tipe nilai, yaitu non-nullable; Anda harus membungkus nilai kembalian metode pembungkus Nullable<T>, yang membuatnya sedikit menyakitkan.

Sekali lagi, saya cukup yakin bahwa akan bermanfaat untuk menulis alat seperti itu dalam skenario Anda, mungkin kecuali jika Anda akan mengerjakan pustaka seperti ini (dapat digunakan secara mulus dari F # dan C # keduanya) secara teratur. Bagaimanapun, saya pikir itu akan menjadi eksperimen yang menarik, yang bahkan mungkin saya jelajahi suatu saat.

ShdNx
sumber
terdengar menarik. Apakah menggunakan Mono.Cecilmean pada dasarnya menulis program untuk memuat perakitan F #, menemukan item yang membutuhkan pemetaan, dan mereka memancarkan perakitan lain dengan pembungkus C #? Apakah saya mendapatkan hak ini?
Philip P.
@Komrade P .: Anda juga dapat melakukannya, tetapi itu jauh lebih rumit, karena Anda harus menyesuaikan semua referensi untuk menunjuk ke anggota majelis baru. Ini jauh lebih sederhana jika Anda menambahkan anggota baru (pembungkus) ke rakitan F # -written yang ada.
ShdNx
2

Draf F # Panduan Desain Komponen (Agustus 2010)

Gambaran Umum Dokumen ini membahas beberapa masalah yang berkaitan dengan desain dan pengkodean komponen F #. Secara khusus, ini mencakup:

  • Panduan untuk mendesain pustaka .NET "vanilla" untuk digunakan dari bahasa .NET apa pun.
  • Panduan untuk pustaka F # -to-F # dan kode implementasi F #.
  • Saran tentang konvensi pengkodean untuk kode implementasi F #
gradbot
sumber