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 #
Jawaban:
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
compose
adalah konsep tingkat yang terlalu rendah dari sudut pandang pustaka .NET. Anda dapat mengeksposnya sebagai metode yang bekerja denganFunc
danAction
, 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.FSharp
danMyLibrary
).Dalam contoh Anda, Anda dapat meninggalkan implementasi F #
MyLibrary.FSharp
dan kemudian menambahkan pembungkus .NET (C # -friendly) (mirip dengan kode yang diposting Daniel) diMyLibrary
namespace sebagai metode statis dari beberapa kelas. Tapi sekali lagi, pustaka .NET mungkin akan memiliki API yang lebih spesifik daripada komposisi fungsi.sumber
Anda hanya perlu membungkus nilai fungsi (fungsi yang diterapkan sebagian, dll) dengan
Func
atauAction
, sisanya akan dikonversi secara otomatis. Sebagai contoh:Jadi masuk akal untuk digunakan
Func
/Action
jika 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:
let
Binding modul + dan tipe tanpa konstruktor + anggota statis tampak persis sama di C #, jadi gunakan modul jika Anda bisa. Anda dapat menggunakanCompiledNameAttribute
untuk memberi anggota C # nama yang ramah.Saya mungkin salah, tapi dugaan saya adalah bahwa Panduan Komponen ditulis sebelum
System.Tuple
ditambahkan ke kerangka kerja. (Dalam versi sebelumnya F # mendefinisikan tipe tupelnya sendiri.) Sejak itu menjadi lebih dapat diterima untuk digunakanTuple
dalam antarmuka publik untuk tipe sepele.Di sinilah saya pikir Anda harus melakukan hal-hal dengan cara C # karena F # bermain dengan baik
Task
tetapi C # tidak bermain dengan baikAsync
. Anda dapat menggunakan async secara internal kemudian memanggilAsync.StartAsTask
sebelum kembali dari metode publik.Embrace of
null
mungkin 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.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.Fungsi curried menghasilkan nilai fungsi , yang tidak boleh digunakan.
Saya merasa lebih baik merangkul null daripada bergantung padanya tertangkap di antarmuka publik dan mengabaikannya secara internal. YMMV.
Sangat masuk akal untuk menggunakan
list
/map
/ secaraset
internal. Mereka semua dapat diekspos melalui antarmuka publik sebagaiIEnumerable<_>
. Juga,seq
,dict
, danSeq.readonly
sering berguna.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.
sumber
module Foo.Bar.Zoo
di C # ini akan beradaclass Foo
di namespaceFoo.Bar
. Namun jika Anda memiliki modul bersarang sepertimodule Foo=... module Bar=... module Zoo==
ini, ini akan menghasilkan kelasFoo
dengan kelas dalamBar
danZoo
. ReIEnumerable
, ini mungkin tidak dapat diterima ketika kita benar-benar perlu bekerja dengan peta dan set. Renull
menghargai danAllowNullLiteral
- salah satu alasan saya suka F # adalah karena saya bosannull
dengan C #, benar-benar tidak ingin mencemari semuanya dengan itu lagi!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 menggunakannull
seperti di C #. Menulis generator pembungkus untuk skenario ini seharusnya cukup mudah: metode pembungkus akan memanggil metode asli: jika nilai kembaliannya adalahSome x
, maka kembalikanx
, atau kembalikannull
.Anda akan perlu menangani kasus ketika
T
merupakan tipe nilai, yaitu non-nullable; Anda harus membungkus nilai kembalian metode pembungkusNullable<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.
sumber
Mono.Cecil
mean 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?Draf F # Panduan Desain Komponen (Agustus 2010)
sumber