Menyediakan F# untuk C#

Menulis pustaka F# yang terbaca wajar bagi pemanggil C#, dari penamaan sampai tipe antarmuka.

Daftar isi

Topik ini membahas cara menyediakan pustaka F# agar terbaca wajar bagi pemanggil C#. Pembahasannya mencakup pustaka F# kecil lengkap dengan berkas proyeknya, proyek konsol C# yang menggunakannya secara nyata, dan aturan pragmatis penamaan serta pemilihan tipe untuk antarmuka publik. Setelah topik ini dibaca, pustaka F# yang berjalan di atas .NET dapat dirancang agar kedua sisi bahasa merasa bekerja dengan pustaka yang alami.

Pustaka F# dan berkas proyeknya#

Pustaka F# yang ramah C# dimulai dari dua keputusan, yaitu penamaan yang mengikuti konvensi .NET dan pemilihan tipe yang dikenali kedua bahasa. Contoh berikut adalah pustaka konversi satuan sederhana yang memuat satu modul fungsi konversi suhu dan satu kelas konverter panjang. Fungsi ditulis dengan satu parameter agar tampil sebagai metode statis biasa, sedangkan member kelasnya mengembalikan tipe .NET standar seperti string dan IReadOnlyList<string>.

fsharp Library.fs
// tidak-dijalankan: pustaka ini dibangun dan digunakan melalui proyek pada bagian berikutnya
namespace SatuanBersama

// Fungsi berparameter tunggal tampil sebagai metode statis biasa bagi pemanggil C#
module KonversiSuhu =
    let CelsiusKeFahrenheit(celsius: float) =
        celsius * 9.0 / 5.0 + 32.0
    let FahrenheitKeCelsius(fahrenheit: float) =
        (fahrenheit - 32.0) * 5.0 / 9.0

// Kelas dengan anggota instance yang mengembalikan tipe .NET standar
type KonverterPanjang() =
    member this.MeterKeKaki(meter: float) =
        meter * 3.28084
    member this.KakiKeMeter(kaki: float) =
        kaki / 3.28084
    member this.SatuanDidukung() =
        [ "meter"; "kaki"; "inci" ]
        :> System.Collections.Generic.IReadOnlyList<string>
    member this.Ringkasan(meter: float) : string =
        let kaki = this.MeterKeKaki(meter)
        sprintf "%g meter = %F kaki" meter kaki

Nama modul KonversiSuhu beserta seluruh fungsinya ditulis dalam PascalCase, sehingga dari kode C# bentuknya terbaca sebagai KonversiSuhu.CelsiusKeFahrenheit. Record, discriminated union, dan fungsi curried sengaja tidak digunakan pada antarmuka publik pustaka ini karena bentuknya sulit diterjemahkan ke sisi C#. Berkas fsproj berikut menyatakan berkas kompilasi tunggal beserta bingkai sasarannya.

xml SatuanBersama.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Library.fs" />
  </ItemGroup>

</Project>

Elemen Compile Include="Library.fs" wajib ada pada proyek F# karena berkas sumber F# tidak disertakan otomatis oleh SDK, berbeda dari proyek C#. Atribut GenerateDocumentationFile memerintahkan compiler menghasilkan berkas dokumentasi XML dari komentar berformat ///, sehingga editor di sisi pemanggil dapat menampilkan keterangan pada setiap member. Bingkai sasarannya mengikuti SDK yang dipasang, yaitu net10.0 pada jalur ini.

Proyek C# pemanggil#

Sisi pemanggil berupa proyek konsol C# yang merujuk proyek F# melalui elemen ProjectReference. Jalur rujukannya relatif terhadap berkas csproj, yaitu ..\SatuanBersama\SatuanBersama.fsproj. Berkas Program.cs berikut menggunakan modul dan kelas pustaka seperti menggunakan kelas .NET mana pun.

csharp Program.cs
// tidak-dijalankan: proyek C# ini dieksekusi melalui blok bash berikutnya
using SatuanBersama;

var keFahrenheit = KonversiSuhu.CelsiusKeFahrenheit(30.0);
Console.WriteLine($"30 C = {keFahrenheit:F1} F");

var keCelsius = KonversiSuhu.FahrenheitKeCelsius(77.0);
Console.WriteLine($"77 F = {keCelsius:F1} C");

var konverter = new KonverterPanjang();
Console.WriteLine(konverter.Ringkasan(100.0));
Console.WriteLine($"5.5 kaki = {konverter.KakiKeMeter(5.5):F2} meter");

foreach (var satuan in konverter.SatuanDidukung())
{
    Console.WriteLine($"Satuan didukung: {satuan}");
}
xml PakaiSatuan.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\SatuanBersama\SatuanBersama.fsproj" />
  </ItemGroup>

</Project>

Dua proyek di atas dibangun dari empat berkas yang telah ditampilkan, dan setiap berkas disalin ke folder SatuanBersama atau PakaiSatuan sesuai namanya.

bash
# menyiapkan folder kedua proyek di dalam direktori kerja
mkdir -p SatuanBersama PakaiSatuan
# menyalin berkas sumber dari daftar di atas ke tempatnya
cp Library.fs SatuanBersama/Library.fs
cp SatuanBersama.fsproj SatuanBersama/SatuanBersama.fsproj
cp Program.cs PakaiSatuan/Program.cs
cp PakaiSatuan.csproj PakaiSatuan/PakaiSatuan.csproj
bash
# membangun dan menjalankan proyek C# yang merujuk pustaka F#
dotnet run --project PakaiSatuan
text Keluaran
30 C = 86.0 F
77 F = 25.0 C
100 meter = 328.084000 kaki
5.5 kaki = 1.68 meter
Satuan didukung: meter
Satuan didukung: kaki
Satuan didukung: inci

Keluaran di atas berasal dari eksekusi nyata kedua proyek tersebut. Fungsi modul tampil sebagai metode statis, konstruktor kelas dipanggil dengan new, member instance dipanggil dengan notasi titik, dan nilai kembalian IReadOnlyList<string> diiterasi dengan foreach. Tidak ada satu pun tipe khusus F# yang muncul pada antarmuka yang digunakan. Karena proyek C# merujuk proyek F#, satu perintah dotnet run membangun pustaka F# terlebih dahulu, lalu membangun dan menjalankan proyek C#.

Proyek C# pada contoh memuat dua pengaturan bawaan templat, yaitu ImplicitUsings yang menyertakan beberapa namespace bawaan secara otomatis dan Nullable yang mengaktifkan analisis null pada kode C#. Keduanya tidak mengubah cara pustaka F# dipanggil, tetapi keduanya memperlihatkan posisi pustaka F# sebagai assembly .NET biasa di mata proyek pemanggilnya.

Aturan pragmatis antarmuka publik#

Perbedaan terbesar terlihat pada fungsi curried, yaitu fungsi yang mengambil satu parameter pada satu waktu dan mengembalikan fungsi yang menerima parameter berikutnya. F# menulis let Tambah (x: float) (y: float) = x + y dengan gaya curried, dan bentuk itu tampil sebagai FSharpFunc bertingkat dari sisi C# sehingga tidak nyaman dipanggil. Bentuk yang ramah C# adalah parameter tunggal atau tuple. Nama modul, tipe, dan member merupakan permukaan pertama yang dilihat pemanggil, sedangkan tipe kembalian menentukan seberapa nyata pustaka itu terasa sebagai pustaka .NET biasa.

csharp Ilustrasi: dua bentuk fungsi F# dari sisi C#
// tidak-dijalankan: ilustrasi bentuk tampilan fungsi F# dari sisi C#
// Bentuk ramah: fungsi berparameter tunggal tampil sebagai metode statis biasa
var keFahrenheit = KonversiSuhu.CelsiusKeFahrenheit(30.0);

// Bentuk sulit: fungsi curried tampil sebagai FSharpFunc bertingkat
// FSharpFunc<double, FSharpFunc<double, double>> tambah = ModulMatematika.Tambah;
// var hasil = FSharpFunc<double, double>.InvokeFast(tambah, 2.0, 3.0);

Aturan pragmatis untuk antarmuka publik pustaka F# dirangkum sebagai berikut.

  • Gunakan bentuk parameter tunggal atau tuple pada API publik, dan simpan gaya curried untuk kode di dalam pustaka.
  • Jangan mengekspos Option<'T>, F# list, maupun discriminated union pada API publik, karena pemanggil C# tidak memiliki dukungan bahasa untuk tipe tersebut. Gunakan string yang dapat bernilai null, array, IReadOnlyList<T>, atau kelas dan antarmuka .NET.
  • Tulis nama tipe, member, dan modul kontainer fungsi dalam PascalCase mengikuti konvensi .NET.
  • Utamakan kelas dan antarmuka .NET standar sebagai kontrak lintas bahasa, karena bentuk itulah yang dipahami kedua compiler.

Aturan tersebut tidak menuntut gaya F# ditinggalkan di dalam pustaka. Logika internal tetap ditulis dengan gaya fungsional penuh, dan lapisan antarmuka publik yang tipis menerjemahkannya ke tipe .NET standar. Penerjemahan semacam itu biasanya hanya beberapa baris per fungsi publik, sedangkan manfaatnya menyeluruh bagi pemanggil lintas bahasa. Pengecualian yang dilempar pustaka F# pun tiba di sisi C# sebagai System.Exception biasa, sehingga penanganannya tidak menuntut jenis khusus apa pun.

Kontrak polimorfisme juga mengikuti aturan yang sama. Antarmuka yang diimplementasikan kelas F#, termasuk antarmuka bawaan seperti IEnumerable<T>, terbaca oleh kode C# sebagai antarmuka biasa, sehingga polimorfisme lintas bahasa paling aman dinyatakan melalui antarmuka, bukan melalui tipe kelas yang menyembunyikan implementasinya. Pendekatan ini sekaligus menghubungkan pustaka dengan seluruh pustaka .NET yang menerima antarmuka tersebut.

Ringkasan#

  • Fungsi F# berparameter tunggal tampil sebagai metode statis bagi C#, sedangkan fungsi curried tampil sebagai FSharpFunc bertingkat yang sulit dipanggil.
  • Tipe antarmuka publik sebaiknya terbatas pada tipe .NET standar seperti string, float, bool, array, dan IReadOnlyList<T>.
  • Proyek C# menggunakan pustaka F# melalui ProjectReference, dan satu perintah dotnet run membangun kedua proyek sekaligus.

Referensi#