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 #, compose
fungsi 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 FSharpFunc
di tanda tangan, apa yang saya inginkan adalah Func
dan Action
sebaliknya, seperti ini:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Untuk mencapai ini, saya dapat membuat compose2
fungsi 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 compose2
dari F #! Sekarang saya harus membungkus semua F # standar funs
ke dalam Func
dan 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:
- Dua rakitan terpisah: satu ditargetkan untuk pengguna F #, dan yang kedua untuk pengguna C #.
- 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:
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.
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 .
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
bar
danzoo
tanpa mengawalnyaFoo
.
Penggunaan tupel
F #: Gunakan tupel jika sesuai untuk nilai yang dikembalikan.
C #: Hindari menggunakan tupel sebagai nilai kembalian di vanilla .NET API.
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 #.
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.
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 #
Fungsi kari
F #: fungsi kari idiomatik untuk F #
C #: Jangan gunakan kari parameter di vanilla .NET API.
Memeriksa nilai nol
F #: ini tidak idiomatis untuk F #
C #: Pertimbangkan untuk memeriksa nilai null pada batas vanilla .NET API.
Penggunaan F jenis #
list
,map
,set
, dllF #: 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
)
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 #