Panduan desain komponen F#

Dokumen ini adalah serangkaian panduan desain komponen untuk pemrograman F#, berdasarkan Panduan Desain Komponen F#, v14, Microsoft Research, dan versi yang awalnya dikumpulkan dan dikelola oleh F# Software Foundation.

Dokumen ini mengasumsikan Anda sudah terbiasa dengan pemrograman F#. Rasa terima kasih sebesar-besarnya terhatur untuk komunitas F# atas berbagai kontribusi dan umpan balik yang bermanfaat pada berbagai versi panduan ini.

Gambaran Umum

Dokumen ini menanggapi beberapa masalah yang terkait dengan desain dan pengodean komponen F#. Komponen dapat berarti salah satu dari hal berikut:

  • Lapisan dalam proyek F# yang memiliki konsumen eksternal di dalamnya.
  • Pustaka yang ditujukan untuk dipakai oleh kode F# di seluruh batas rakitan.
  • Pustaka yang ditujukan untuk dipakai oleh bahasa pemrogram .NET mana pun di seluruh batas rakitan.
  • Pustaka yang ditujukan untuk distribusi melalui repositori paket, seperti NuGet.

Berbagai teknik yang dijelaskan dalam artikel ini mengikuti Lima prinsip kode F# yang baik, dan oleh karenanya menggunakan baik pemrograman fungsi dan objek dengan sebagaimana mestinya.

Terlepas dari metodologinya, komponen dan desainer pustaka menghadapi sejumlah masalah praktis dan prosaik saat mencoba membuat API yang paling mudah digunakan oleh para pengembang. Penerapan secara cermat dari Panduan Desain Pustaka .NET akan mengarahkan Anda untuk membuat sekumpulan API konsisten yang menyenangkan untuk dipakai.

Panduan umum

Ada beberapa panduan universal yang berlaku untuk pustaka F#, terlepas dari audiens yang dimaksudkan untuk pustaka.

Pelajari Panduan Desain Pustaka .NET

Terlepas dari jenis pengodean F# yang Anda lakukan, pastilah sangat berharga untuk memiliki pengetahuan kerja tentang Panduan Desain Pustaka .NET. Sebagian besar programmer F# dan .NET lainnya akan terbiasa dengan panduan ini, dan mengharapkan kode .NET sesuai dengan mereka.

Panduan Desain Pustaka .NET memberikan panduan umum mengenai penamaan, desain kelas dan antarmuka, desain anggota (properti, metode, peristiwa, dll.) dan banyak lagi, serta merupakan titik referensi awal yang berguna untuk berbagai panduan desain.

Menambahkan komentar dokumentasi XML ke kode Anda

Dokumentasi XML pada API publik memastikan bahwa pengguna bisa mendapatkan Intellisense dan Quickinfo yang hebat saat menggunakan berbagai jenis dan anggota ini, serta memungkinkan pembuatan file dokumentasi untuk pustaka. Lihat Dokumentasi XML tentang berbagai tag xml yang dapat digunakan untuk markup tambahan dalam komentar xmldoc.

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

Anda dapat menggunakan baik komentar XML formulir pendek (/// comment), atau komentar XML standar (///<summary>comment</summary>).

Pertimbangkan penggunaan file tanda tangan eksplisit (.fsi) untuk pustaka dan komponen API stabil

Penggunaan file tanda tangan eksplisit di pustaka F# memberikan ringkasan API publik yang sesingkatnya, yang membantu memastikan bahwa Anda mengetahui permukaan publik lengkap dari pustaka Anda, dan memberikan pemisahan yang jelas antara dokumentasi publik dan detail penerapan internal. File tanda tangan menambahkan friksi untuk mengubah API publik, dengan mengharuskan perubahan dilakukan baik dalam file implementasi dan tanda tangan. Akibatnya, file tanda tangan biasanya hanya boleh diperkenalkan ketika API telah diperkuat dan tidak lagi diharapkan berubah secara signifikan.

Ikuti praktik terbaik untuk menggunakan string di .NET

Ikuti Praktik Terbaik untuk Menggunakan String dalam panduan .NET saat cakupan proyek menjaminnya. Secara khusus, secara eksplisit menyatakan niat budaya dalam konversi dan perbandingan string (jika berlaku).

Panduan pustaka F#-facing

Bagian ini menyajikan rekomendasi pengembangan pustaka F#-facing publik; artinya, pustaka yang mengekspos API publik yang ditujukan untuk dipakai oleh pengembang F#. Ada berbagai rekomendasi desain pustaka yang berlaku khusus untuk F#. Dengan ketiadaan rekomendasi spesifik yang mengikutinya, Panduan Desain Pustaka .NET merupakan panduan fallback.

Konvensi penamaan

Gunakan konvensi penamaan dan kapitalisasi .NET

Tabel berikut mengikuti konvensi penamaan dan kapitalisasi .NET. Ada tambahan kecil untuk turut menyertakan konstruksi F#. Rekomendasi tersebut terutama dimaksudkan untuk API yang melintasi luar batas F#-ke-F#, cocok dengan idiom dari .NET BCL dan sebagian besar pustaka.

Konstruksi Case Bagian Contoh Catatan
Jenis konkret PascalCase Kata benda/kata sifat Daftar, Ganda, Kompleks Jenis konkret adalah struct, kelas, enumerasi, delegasi, rekaman, dan gabungan. Meskipun nama jenis biasanya menggunakan huruf kecil di OCaml, F# telah mengadopsi skema penamaan .NET untuk jenis.
DLL PascalCase Fabrikam.Core.dll
Tag gabungan PascalCase Kata benda Beberapa, Tambahkan, Berhasil Jangan gunakan awalan di API publik. Secara opsional gunakan awalan saat internal, seperti "ketik Teams = TAlpha | TBeta | TDelta".
Kejadian PascalCase Kata kerja ValueChanged / ValueChanging
Pengecualian PascalCase WebException Nama harus diakhiri dengan "Pengecualian".
Bidang PascalCase Kata benda CurrentName
Jenis antarmuka PascalCase Kata benda/kata sifat IDisposable Nama harus diawali dengan "I".
Metode PascalCase Kata kerja ToString
Ruang nama PascalCase Microsoft.FSharp.Core Umumnya menggunakan <Organization>.<Technology>[.<Subnamespace>], meskipun menghilangkan organisasi jika teknologinya independen dari organisasi.
Parameter camelCase Kata benda typeName, transform, range
nilai let (internal) camelCase atau PascalCase Kata benda/kata kerja getValue, myTable
nilai let (eksternal) camelCase atau PascalCase Kata benda/kata kerja List.map, Dates.Today nilai let-bound sering kali bersifat publik ketika mengikuti pola desain fungsional tradisional. Namun, umumnya gunakan PascalCase ketika pengidentifikasi dapat digunakan dari bahasa pemrogram .NET lainnya.
Properti PascalCase Kata benda/kata sifat IsEndOfFile, BackColor Properti Boolean umumnya menggunakan Is dan Can serta harus afirmatif, seperti dalam IsEndOfFile, bukan IsNotEndOfFile.

Hindari singkatan

Panduan .NET mencegah penggunaan singkatan (misalnya, "gunakan OnButtonClick daripada OnBtnClick"). Singkatan umum, seperti Async untuk "Asinkron", ditoleransi. Panduan ini terkadang diabaikan untuk pemrograman fungsional; misalnya, List.iter menggunakan singkatan untuk "iterate". Untuk alasan ini, menggunakan singkatan cenderung ditoleransi ke tingkat yang lebih besar dalam pemrograman F#-ke-F#, tetapi umumnya masih harus dihindari dalam desain komponen publik.

Hindari tabrakan nama kapitalisasi

Panduan .NET mengatakan bahwa kapitalisasi saja tidak dapat digunakan untuk membedakan tabrakan nama, karena beberapa bahasa pemrogram klien (misalnya, Visual Basic) tidak peka huruf besar/kecil.

Gunakan akronim jika sesuai

Akronim seperti XML bukanlah singkatan dan banyak digunakan dalam pustaka .NET dalam bentuk yang tidak dikapitalisasi (Xml). Hanya akronim yang terkenal dan dikenal luas yang harus digunakan.

Gunakan PascalCase untuk nama parameter generik

Jangan gunakan PascalCase untuk nama parameter generik di API publik, termasuk untuk pustaka F#-facing. Secara khusus, gunakan nama seperti T, U, T1, T2 untuk parameter generik arbitrer, dan ketika nama tertentu masuk akal, maka untuk pustaka F#-facing gunakan nama seperti Key, Value, Arg (tetapi tidak misalnya, TKey).

Gunakan PascalCase atau camelCase untuk fungsi dan nilai publik dalam modul F#

CamelCase digunakan untuk fungsi publik yang didesain untuk digunakan agar tidak memenuhi syarat (misalnya, invalidArg), dan untuk "fungsi koleksi standar" (misalnya, List.map). Dalam kedua kasus ini, nama fungsi bertindak seperti kata kunci dalam bahasa pemrogram.

Desain Objek, Jenis, dan Modul

Gunakan namespace layanan atau modul untuk memuat jenis dan modul Anda

Setiap file F# dalam komponen harus dimulai dengan deklarasi namespace layanan atau deklarasi modul.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Perbedaan antara menggunakan modul dan namespace layanan untuk mengatur kode di tingkat atas adalah sebagai berikut:

  • Namespace layanan dapat menjangkau banyak file
  • Namespace layanan tidak boleh berisi fungsi F# kecuali berada dalam modul dalam
  • Kode untuk modul tertentu harus terkandung dalam satu file
  • Modul tingkat atas dapat berisi fungsi F# tanpa memerlukan modul dalam

Pilihan antara namespace layanan atau modul tingkat atas memengaruhi bentuk kode yang dikompilasi, dan oleh karenanya akan memengaruhi tampilan dari bahasa pemrogram .NET lainnya jika API Anda akhirnya dipakai di luar kode F#.

Gunakan metode dan properti untuk operasi intrinsik ke jenis objek

Saat bekerja dengan objek, yang terbaik adalah memastikan bahwa fungsionalitas yang dapat dipakai diimplementasikan sebagai metode dan properti pada jenis tersebut.

type HardwareDevice() =

    member this.ID = ...

    member this.SupportedProtocols = ...

type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =

    member this.Add(key, value) = ...

    member this.ContainsKey(key) = ...

    member this.ContainsValue(value) = ...

Sebagian besar fungsionalitas untuk anggota tertentu tidak perlu diimplementasikan dalam anggota tersebut, tetapi bagian yang dapat dipakai dari fungsionalitas tersebut harus.

Gunakan kelas untuk menyertakan status yang dapat diubah

Dalam F#, ini hanya perlu dilakukan saat status tersebut belum disertakan oleh konstruksi bahasa pemrogram lain, seperti penutupan, ekspresi urutan, atau komputasi asinkron.

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

Gunakan jenis antarmuka untuk mewakili serangkaian operasi. Ini lebih disukai daripada opsi lain, seperti tupel fungsi atau rekaman fungsi.

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

Disukai untuk:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

Antarmuka adalah konsep kelas satu di dalam .NET, yang dapat Anda gunakan untuk mencapai apa yang biasanya diberikan Functors kepada Anda. Selain itu, ia dapat digunakan untuk mengodekan jenis eksistensial ke dalam program Anda, rekaman fungsi mana yang tidak dapat.

Gunakan modul untuk mengelompokkan fungsi yang bertindak pada koleksi

Saat Anda menentukan jenis koleksi, pertimbangkan untuk menyediakan serangkaian operasi standar seperti CollectionType.map dan CollectionType.iter) untuk jenis koleksi baru.

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

Jika Anda menyertakan modul seperti itu, ikuti konvensi penamaan standar untuk fungsi yang ditemukan di FSharp.Core.

Gunakan modul untuk mengelompokkan fungsi untuk fungsi kanonis umum, terutama dalam pustaka matematika dan DSL

Misalnya, Microsoft.FSharp.Core.Operators merupakan koleksi fungsi tingkat atas yang dibuka secara otomatis (seperti abs dan sin) yang disediakan oleh FSharp.Core.dll.

Demikian juga, pustaka statistik mungkin menyertakan modul dengan fungsi erf dan erfc, di mana modul ini didesain untuk dibuka secara eksplisit atau otomatis.

Pertimbangkan penggunaan RequireQualifiedAccess dan terapkan atribut AutoOpen dengan hati-hati

Menambahkan atribut [<RequireQualifiedAccess>] ke modul menunjukkan bahwa modul tidak dapat dibuka dan referensi ke elemen modul memerlukan akses yang memenuhi syarat secara eksplisit. Misalnya, modul Microsoft.FSharp.Collections.List memiliki atribut ini.

Ini berguna ketika fungsi dan nilai dalam modul memiliki nama yang cenderung bertentangan dengan nama di modul lain. Mengharuskan akses yang memenuhi syarat dapat sangat meningkatkan kemampuan pemeliharaan jangka panjang dan kemampuan pustaka untuk berkembang.

Sangat disarankan untuk memiliki [<RequireQualifiedAccess>] atribut untuk modul kustom yang memperluas yang disediakan oleh FSharp.Core (seperti Seq, , ListArray), karena modul tersebut sebelumnya digunakan dalam kode F# dan telah [<RequireQualifiedAccess>] ditentukan pada mereka; lebih umum, tidak disarankan untuk menentukan modul kustom yang tidak memiliki atribut, ketika modul tersebut bayangan atau memperluas modul lain yang memiliki atribut.

Menambahkan atribut [<AutoOpen>] ke modul berarti modul akan dibuka saat namespace layanan yang berisi dibuka. Atribut [<AutoOpen>] juga dapat diterapkan ke rakitan untuk menunjukkan modul yang dibuka secara otomatis saat rakitan direferensikan.

Misalnya, pustaka statistik MathsHeaven.Statistics mungkin berisi module MathsHeaven.Statistics.Operators yang berisi fungsi erf dan erfc. Merupakan hal yang masuk akal untuk menandai modul ini sebagai [<AutoOpen>]. Ini berarti open MathsHeaven.Statistics juga akan membuka modul ini dan membawa nama erf dan erfc ke dalam cakupan. Penggunaan [<AutoOpen>] lainnya yang baik adalah untuk modul yang berisi metode ekstensi.

Penggunaan [<AutoOpen>] yang berlebihan menyebabkan namespace layanan tercemari, dan atributnya harus digunakan dengan hati-hati. Untuk pustaka tertentu di domain tertentu, penggunaan [<AutoOpen>] yang beralasan dapat menghasilkan kegunaan yang lebih baik.

Pertimbangkan untuk menentukan anggota operator pada berbagai kelas lokasi penggunaan operator terkenal sesuai

Terkadang kelas digunakan untuk pemodelan konstruksi matematika seperti Vektor. Ketika domain yang dimodelkan memiliki operator terkenal, menentukannya sebagai anggota intrinsik ke kelas sangat membantu.

type Vector(x: float) =

    member v.X = x

    static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)

    static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)

let v = Vector(5.0)

let u = v * 10.0

Panduan ini sesuai dengan panduan .NET umum untuk berbagai jenis ini. Namun, mungkin juga penting dalam pengodean F# karena ini memungkinkan berbagai jenis ini digunakan bersama dengan fungsi dan metode F# dengan batasan anggota, seperti List.sumBy.

Pertimbangkan untuk menggunakan CompiledName untuk menyediakan Nama .NET yang ramah untuk konsumen bahasa pemrogram .NET lainnya

Terkadang Anda mungkin ingin menamai sesuatu dalam satu gaya untuk konsumen F# (seperti anggota statis dalam huruf kecil sehingga muncul seolah-olah itu adalah fungsi yang terikat modul), tetapi memiliki gaya yang berbeda untuk penamaan ketika dikompilasi menjadi rakitan. Anda dapat menggunakan atribut [<CompiledName>] untuk memberikan gaya yang berbeda untuk kode non F# yang memakai rakitan.

type Vector(x:float, y:float) =

    member v.X = x
    member v.Y = y

    [<CompiledName("Create")>]
    static member create x y = Vector (x, y)

let v = Vector.create 5.0 3.0

Dengan menggunakan [<CompiledName>], Anda dapat menggunakan konvensi penamaan .NET untuk konsumen non F# dari rakitan.

Gunakan metode kelebihan beban untuk fungsi anggota, jika melakukannya memberikan API yang lebih sederhana

Metode kelebihan beban adalah alat yang ampuh untuk menyederhanakan API yang mungkin dibutuhkan melakukan fungsionalitas serupa, tetapi dengan opsi atau argumen yang berbeda.

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

Dalam F#, lebih umum untuk kelebihan beban pada jumlah argumen daripada jenis argumen.

Sembunyikan representasi jenis rekaman dan gabungan jika desain berbagai jenis ini kemungkinan akan berkembang

Hindari pengungkapan representasi konkret objek. Misalnya, representasi konkret nilai DateTime tidak diungkapkan oleh API publik eksternal dari desain pustaka .NET. Pada durasi, Common Language Runtime mengetahui implementasi berkomitmen yang akan digunakan selama eksekusi. Namun, kode yang dikompilasi tidak mengambil sendiri dependensi pada representasi konkret.

Hindari penggunaan pewarisan implementasi untuk ekstensibilitas

Dalam F#, pewarisan implementasi jarang digunakan. Selain itu, hierarki pewarisan sering kali kompleks dan sulit diubah ketika persyaratan baru tiba. Implementasi pewarisan masih ada di F# untuk kompatibilitas dan kasus langka di mana ini adalah solusi terbaik untuk masalah, tetapi teknik alternatif harus dicari dalam program F# Anda saat merancang polimorfisme, seperti implementasi antarmuka.

Tanda tangan fungsi dan anggota

Gunakan tuple untuk nilai yang dikembalikan saat mengembalikan sejumlah kecil nilai yang tidak terkait

Berikut adalah contoh penggunaan tuple yang baik dalam jenis pengembalian:

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

Untuk jenis pengembalian yang berisi banyak komponen, atau saat komponennya terkait dengan satu entitas yang dapat diidentifikasi, pertimbangkan untuk menggunakan jenis bernama alih-alih sebuah tuple.

Gunakan Async<T> untuk pemrograman asinkron di batasan F# API

Jika ada operasi sinkron terkait bernama Operation yang mengembalikan T, maka operasi asinkron harus dinamai AsyncOperation jika operasi tersebut mengembalikan Async<T> atau OperationAsync jika mengembalikan Task<T>. Untuk jenis .NET yang umum digunakan yang mengekspos metode Begin/End, pertimbangkan menggunakan Async.FromBeginEnd untuk menulis metode ekstensi sebagai fasad untuk memberikan model pemrograman asinkron F# ke API .NET tersebut.

type SomeType =
    member this.Compute(x:int): int =
        ...
    member this.AsyncCompute(x:int): Async<int> =
        ...

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        ...

Pengecualian

Lihat Manajemen Kesalahan untuk mempelajari tentang penggunaan pengecualian, hasil, dan opsi yang sesuai.

Anggota Ekstensi

Terapkan anggota ekstensi F# dengan hati-hati di komponen F#-ke-F#

Anggota ekstensi F# umumnya hanya boleh digunakan untuk operasi yang berada dalam penutupan operasi intrinsik yang terkait dengan jenis dalam sebagian besar mode penggunaannya. Salah satu penggunaan umum adalah memberikan API yang lebih idiomatik untuk F# bagi berbagai jenis .NET:

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        Async.FromBeginEnd(this.BeginReceive, this.EndReceive)

type System.Collections.Generic.IDictionary<'Key,'Value> with
    member this.TryGet key =
        let ok, v = this.TryGetValue key
        if ok then Some v else None

Jenis Gabungan

Gunakan gabungan yang didiskriminasi daripada hierarki kelas untuk data terstruktur pohon

Struktur seperti pohon didefinisikan secara rekursif. Ini canggung dengan pewarisan, tetapi elegan dengan Gabungan yang Diskriminasi.

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

Mewakili data seperti pohon dengan Gabungan yang Diskriminasi juga memungkinkan Anda untuk mendapatkan manfaat dari kelelahan dalam pencocokan pola.

Gunakan [<RequireQualifiedAccess>] pada jenis gabungan yang huruf namanya tidak cukup unik

Anda mungkin menemukan diri Anda di domain di mana nama yang sama adalah nama terbaik untuk hal-hal yang berbeda, seperti kasus Gabungan yang Diskriminasi. Anda dapat menggunakan [<RequireQualifiedAccess>] untuk membedakan huruf dalam nama untuk menghindari memicu kesalahan yang membingungkan karena bayangan tergantung pada urutan pernyataan open

Sembunyikan representasi gabungan yang didiskriminasi untuk API yang kompatibel dengan biner jika desain jenis ini kemungkinan akan berkembang

Jenis gabungan mengandalkan bentuk pencocokan pola F# untuk model pemrograman yang singkat. Seperti disebutkan sebelumnya, Anda harus menghindari pengungkapan representasi data konkret jika desain jenis ini cenderung berkembang.

Misalnya, representasi gabungan yang didiskriminasi dapat disembunyikan menggunakan deklarasi privat atau internal, atau dengan menggunakan file tanda tangan.

type Union =
    private
    | CaseA of int
    | CaseB of string

Jika Anda mengungkapkan gabungan yang diskriminasi tanpa alasan, Anda mungkin merasa sulit untuk membuat versi pustaka Anda tanpa melanggar kode pengguna. Sebagai gantinya, pertimbangkan pengungkapan satu atau beberapa pola aktif untuk mengizinkan pencocokan pola atas nilai jenis Anda.

Pola aktif menyediakan cara alternatif untuk memberi konsumen F# pencocokan pola sambil menghindari mengekspos Jenis F# Gabungan secara langsung.

Fungsi Sebaris dan Batasan Anggota

Tentukan algoritma numerik generik dengan menggunakan fungsi sebaris yang disertai batasan anggota tersirat dan jenis generik yang diselesaikan secara statis

Batasan anggota aritmatika dan batasan perbandingan F# merupakan hal standar untuk pemrograman F#. Sebagai contoh, perhatikan kode berikut:

let inline highestCommonFactor a b =
    let rec loop a b =
        if a = LanguagePrimitives.GenericZero<_> then b
        elif a < b then loop a (b - a)
        else loop (a - b) b
    loop a b

Jenis fungsi ini adalah seperti berikut ini:

val inline highestCommonFactor : ^T -> ^T -> ^T
                when ^T : (static member Zero : ^T)
                and ^T : (static member ( - ) : ^T * ^T -> ^T)
                and ^T : equality
                and ^T : comparison

Ini merupakan fungsi yang cocok untuk API publik di pustaka matematika.

Hindari menggunakan batasan anggota untuk mensimulasikan kelas jenis dan pengetikan bebek

Dimungkinkan untuk melakukan simulasi "pengetikan bebek" dengan menggunakan batasan anggota F#. Namun, anggota yang menggunakan ini secara umum tidak boleh digunakan dalam desain pustaka F#-ke-F#. Ini karena desain pustaka berdasarkan batasan implisit yang tidak dikenal atau non-standar cenderung menyebabkan kode pengguna menjadi tidak fleksibel dan terikat dengan satu pola kerangka kerja tertentu.

Selain itu, ada kemungkinan besar bahwa penggunaan batasan anggota yang berlebihan dengan cara ini dapat mengakibatkan waktu kompilasi yang sangat lama.

Definisi Operator

Hindari mendefinisikan operator simbolis kustom

Operator kustom sangat penting dalam beberapa situasi dan merupakan perangkat notasi yang sangat berguna dalam isi kode implementasi yang besar. Bagi pengguna baru pustaka, fungsi bernama sering kali lebih mudah digunakan. Selain itu, operator simbolis kustom bisa menjadi sulit di dokumentasikan, dan pengguna merasa lebih sulit untuk mencari bantuan pada operator, karena keterbatasan yang ada di dalam IDE dan mesin pencari.

Akibatnya, yang terbaik adalah dengan menerbitkan fungsionalitas Anda sebagai fungsi dan anggota bernama, dan juga mengekspos operator untuk fungsionalitas ini hanya jika manfaat notasi melebihi dokumentasi dan biaya kognitif untuk memilikinya.

Satuan Ukuran

Gunakan unit pengukuran dengan cermat untuk keamanan jenis tambahan dalam kode F#

Informasi pengetikan tambahan untuk unit pengukuran dihapus saat dilihat oleh bahasa pemrogram .NET lainnya. Ketahuilah bahwa komponen, alat, dan refleksi .NET akan melihat unit-tanpa-jenis. Misalnya, konsumen C# akan melihat float sebagai ganti float<kg>.

Singkatan Jenis

Gunakan singkatan jenis dengan cermat untuk menyederhanakan kode F#

Komponen, alat, dan refleksi .NET tidak akan melihat nama yang disingkat untuk jenis. Penggunaan singkatan jenis yang signifikan juga dapat membuat domain tampak lebih kompleks daripada yang seharusnya, yang dapat membingungkan konsumen.

Hindari singkatan jenis untuk jenis publik yang anggota dan propertinya harus berbeda secara intrinsik dengan yang tersedia pada jenis yang disingkat

Dalam hal ini, jenis yang disingkat mengungkapkan terlalu banyak tentang representasi jenis aktual yang telah ditentukan. Sebagai gantinya, pertimbangkan untuk membungkus singkatan dalam jenis kelas atau gabungan yang didiskriminasi satu kasus (atau, ketika performa sangat penting, pertimbangkan untuk menggunakan jenis struct untuk membungkus singkatan).

Contohnya, sangat menggoda untuk mendefinisikan multi-peta sebagai kasus khusus peta F#, misalnya:

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

Namun, operasi notasi titik logis pada jenis ini tidak sama dengan operasi pada Peta – misalnya, wajar bahwa operator pencarian map[key] mengembalikan daftar kosong jika kunci tidak ada dalam kamus, daripada menaikkan pengecualian.

Panduan pustaka untuk Penggunaan dari Bahasa Pemrogram .NET lainnya

Saat merancang pustaka untuk penggunaan dari bahasa pemrogram .NET lainnya, penting untuk mematuhi Panduan Desain Pustaka .NET. Dalam dokumen ini, berbagai pustaka ini diberi label sebagai pustaka vanilla .NET, berkebalikan dengan pustaka F#-facing yang menggunakan konstruksi F# tanpa batasan. Merancang pustaka vanilla .NET berarti menyediakan API tidak asing dan idiomatik yang konsisten terhadap sisa .NET Framework dengan meminimalkan penggunaan konstruksi khusus F# di API publik. Aturan ini dijelaskan di bagian berikut.

Desain Namespace Layanan dan Jenis (bagi pustaka untuk penggunaan dari Bahasa Pemrogram .NET lainnya)

Menerapkan konvensi penamaan .NET ke API publik komponen Anda

Perhatikan secara khusus penggunaan nama yang disingkat dan panduan kapitalisasi .NET.

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

Menggunakan namespace layanan, jenis, dan anggota sebagai struktur organisasi utama untuk komponen Anda

Semua file yang berisi fungsionalitas publik harus dimulai dengan deklarasi namespace, dan satu-satunya entitas yang menghadap publik di namespace layanan harus jenis. Jangan gunakan modul F#.

Gunakan modul non-publik untuk menyimpan kode implementasi, jenis utilitas, dan fungsi utilitas.

Jenis statis harus lebih disukai daripada modul, karena memungkinkan perkembangan API di masa mendatang untuk menggunakan kelebihan beban dan konsep desain .NET API lainnya yang mungkin tidak digunakan dalam modul F#.

Misalnya, sebagai ganti API publik berikut:

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

Sebagai gantinya, pertimbangkan:

namespace Fabrikam

[<AbstractClass; Sealed>]
type Utilities =
    static member Name = "Bob"
    static member Add(x,y) = x + y
    static member Add(x,y,z) = x + y + z

Gunakan jenis rekaman F# di API .NET vanilla jika desain jenis tidak akan berkembang

Jenis rekaman F# dikompilasi ke kelas .NET yang sederhana. Semua ini cocok untuk beberapa jenis yang sederhana dan stabil dalam API. Pertimbangkan untuk menggunakan atribut [<NoEquality>] dan [<NoComparison>] untuk menekan pembuatan antarmuka otomatis. Juga hindari penggunaan bidang rekaman yang dapat diubah di API .NET vanilla karena ini mengekspos bidang publik. Selalu pertimbangkan apakah kelas akan memberikan opsi yang lebih fleksibel untuk perkembangan API di masa mendatang.

Misalnya, kode F# berikut mengekspos API publik ke konsumen C#:

F#:

[<NoEquality; NoComparison>]
type MyRecord =
    { FirstThing: int
        SecondThing: string }

C#:

public sealed class MyRecord
{
    public MyRecord(int firstThing, string secondThing);
    public int FirstThing { get; }
    public string SecondThing { get; }
}

Sembunyikan representasi jenis gabungan F# di API .NET vanilla

Jenis gabungan F# umumnya tidak digunakan di seluruh batas komponen, bahkan untuk pengodean F#-ke-F#. Jenis ini adalah perangkat implementasi yang sangat baik ketika digunakan secara internal dalam komponen dan pustaka.

Saat merancang API .NET vanilla, pertimbangkan untuk menyembunyikan representasi jenis gabungan dengan menggunakan deklarasi privat atau file tanda tangan.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

Anda juga dapat menambah jenis yang menggunakan representasi gabungan secara internal dengan anggota untuk menyediakan API yang menghadap .NET yang diinginkan.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

    /// A public member for use from C#
    member x.Evaluate =
        match x with
        | And(a,b) -> a.Evaluate && b.Evaluate
        | Not a -> not a.Evaluate
        | True -> true

    /// A public member for use from C#
    static member CreateAnd(a,b) = And(a,b)

Merancang GUI dan komponen lain menggunakan pola desain kerangka kerja

Ada banyak kerangka kerja berbeda yang tersedia dalam .NET, seperti WinForms, WPF, dan ASP.NET. Konvensi penamaan dan desain untuk tiap kerangka kerja harus digunakan jika Anda merancang komponen untuk digunakan dalam kerangka kerja ini. Misalnya, untuk pemrograman WPF, adopsi pola desain WPF untuk kelas yang Anda rancang. Untuk model dalam pemrograman antarmuka pengguna, gunakan pola desain seperti peristiwa dan koleksi berbasis pemberitahuan seperti yang ditemukan di System.Collections.ObjectModel.

Desain Objek dan Anggota (bagi pustaka untuk penggunaan dari Bahasa Pemrogram .NET lainnya)

Gunakan atribut CLIEvent untuk mengekspos peristiwa .NET

Buat DelegateEvent dengan jenis delegasi .NET tertentu yang mengambil objek dan EventArgs (bukan sebuah Event, yang hanya menggunakan jenis FSharpHandler secara default) sehingga peristiwa diterbitkan dengan cara yang familier ke bahasa pemrogram .NET lainnya.

type MyBadType() =
    let myEv = new Event<int>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

type MyEventArgs(x: int) =
    inherit System.EventArgs()
    member this.X = x

    /// A type in a component designed for use from other .NET languages
type MyGoodType() =
    let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

Ekspos operasi asinkron sebagai metode yang mengembalikan tugas .NET

Tugas digunakan dalam .NET untuk mewakili komputasi asinkron aktif. Tugas umumnya kurang komposisional daripada objek F# Async<T>, karena mewakili tugas "sudah mengeksekusi" dan tidak dapat disusun bersama dengan cara yang melakukan komposisi paralel, atau yang menyembunyikan penyebaran sinyal pembatalan dan parameter kontekstual lainnya.

Namun, terlepas dari ini, metode yang mengembalikan Tugas merupakan representasi standar pemrograman asinkron pada .NET.

/// A type in a component designed for use from other .NET languages
type MyType() =

    let compute (x: int): Async<int> = async { ... }

    member this.ComputeAsync(x) = compute x |> Async.StartAsTask

Anda juga akan sering ingin menerima token pembatalan eksplisit:

/// A type in a component designed for use from other .NET languages
type MyType() =
    let compute(x: int): Async<int> = async { ... }
    member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)

Gunakan jenis delegasi .NET sebagai ganti jenis fungsi F#

Di sini "Jenis fungsi F#" berarti jenis "panah" seperti int -> int.

Alih-alih ini:

member this.Transform(f: int->int) =
    ...

Lakukan ini:

member this.Transform(f: Func<int,int>) =
    ...

Jenis fungsi F# muncul sebagai class FSharpFunc<T,U> ke bahasa pemrogram .NET lainnya, dan kurang cocok untuk fitur bahasa dan alat yang memahami jenis delegasi. Saat melakukan penulisan metode dengan urutan yang lebih tinggi yang menargetkan .NET Framework 3.5 atau lebih tinggi, delegasi System.Func dan System.Action adalah API yang tepat untuk diterbitkan agar para pengembang .NET dapat memakai API ini dengan cara gesekan rendah. (Saat menargetkan .NET Framework 2.0, jenis delegasi yang ditentukan sistem lebih terbatas; pertimbangkan untuk menggunakan jenis delegasi yang telah ditentukan sebelumnya seperti System.Converter<T,U> atau tentukan jenis delegasi tertentu.)

Di sisi sebaliknya, delegasi .NET tidak bersifat alami untuk pustaka F#-facing (lihat Bagian berikutnya tentang pustaka F#-facing). Akibatnya, strategi implementasi umum saat mengembangkan metode dengan urutan yang lebih tinggi untuk pustaka vanilla .NET adalah menulis semua implementasi menggunakan jenis fungsi F#, lalu membuat API publik menggunakan delegasi sebagai fasad tipis di atas implementasi F# yang sebenarnya.

Gunakan pola TryGetValue alih-alih mengembalikan nilai opsi F#, dan pilih metode kelebihan beban untuk mengambil nilai opsi F# sebagai argumen

Pola penggunaan umum untuk jenis opsi F# dalam API lebih baik diimplementasikan dalam API .NET vanilla yang menggunakan teknik desain .NET standar. Daripada mengembalikan nilai opsi F#, pertimbangkan untuk menggunakan jenis pengembalian bool ditambah parameter keluar seperti dalam pola "TryGetValue". Dan alih-alih mengambil nilai opsi F# sebagai parameter, pertimbangkan untuk menggunakan metode kelebihan beban atau argumen opsional.

member this.ReturnOption() = Some 3

member this.ReturnBoolAndOut(outVal: byref<int>) =
    outVal <- 3
    true

member this.ParamOption(x: int, y: int option) =
    match y with
    | Some y2 -> x + y2
    | None -> x

member this.ParamOverload(x: int) = x

member this.ParamOverload(x: int, y: int) = x + y

Gunakan jenis antarmuka koleksi .NET IEnumerable<T> dan IDictionary<Key,Value> untuk parameter dan nilai yang dikembalikan

Hindari penggunaan jenis koleksi konkret seperti array T[].NET, jenis F# list<T>, Map<Key,Value> dan Set<T>, serta jenis koleksi konkret .NET seperti Dictionary<Key,Value>. Panduan Desain Pustaka .NET memiliki saran yang baik mengenai kapan harus menggunakan berbagai jenis koleksi seperti IEnumerable<T>. Beberapa penggunaan array (T[]) dapat diterima dalam beberapa keadaan, dengan alasan performa. Perhatikan terutama bahwa seq<T> hanya alias F# untuk IEnumerable<T>, dan oleh karenanya seq sering kali merupakan jenis yang sesuai untuk Vanilla .NET API.

Daripada daftar F#:

member this.PrintNames(names: string list) =
    ...

Gunakan urutan F#:

member this.PrintNames(names: seq<string>) =
    ...

Gunakan jenis unit sebagai satu-satunya jenis input metode untuk menentukan metode nol argumen, atau sebagai satu-satunya jenis pengembalian untuk menentukan metode pengembalian void

Hindari penggunaan lain dari jenis unit. Semua ini bagus:

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

Semua ini buruk:

member this.WrongUnit( x: unit, z: int) = ((), ())

Periksa nilai null pada batas Vanilla .NET API

Kode implementasi F# cenderung memiliki lebih sedikit nilai null, karena pola desain yang tidak dapat diubah dan pembatasan pada penggunaan literal null untuk jenis F#. Bahasa pemrogram .NET lainnya sering menggunakan null sebagai nilai dengan lebih sering. Karenanya, kode F# yang mengekspos vanila API .NET harus memeriksa parameter untuk null di batas API, dan mencegah nilai-nilai ini mengalir lebih dalam ke dalam kode implementasi F#. Fungsi isNull atau pencocokan pola pada pola null dapat digunakan.

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull` argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

Hindari menggunakan tupel sebagai nilai pengembalian

Sebagai gantinya, pilihlah untuk mengembalikan jenis bernama yang menyimpan data agregat, atau menggunakan parameter keluar untuk mengembalikan beberapa nilai. Meskipun tuple dan tuple struct ada di .NET (termasuk dukungan bahasa pemrogram C# untuk tuple struct), keduanya biasanya tidak akan menyediakan API yang ideal dan diharapkan untuk pengembang .NET.

Hindari penggunaan kari parameter

Sebagai gantinya, gunakan konvensi panggilan .NET Method(arg1,arg2,…,argN).

member this.TupledArguments(str, num) = String.replicate num str

Tips: Jika Anda merancang pustaka untuk digunakan dari bahasa pemrogram .NET apa pun, maka tidak ada pengganti untuk benar-benar melakukan beberapa pemrograman C# dan Visual Basic eksperimental untuk memastikan bahwa pustaka Anda "merasa benar" dari bahasa-bahasa ini. Anda juga dapat menggunakan alat seperti .NET Reflector dan Visual Studio Object Browser untuk memastikan bahwa pustaka dan dokumentasinya muncul seperti yang diharapkan oleh para pengembang.

Lampiran

Contoh ujung-ke-ujung perancangan kode F# untuk digunakan oleh bahasa pemrogram .NET lainnya

Pertimbangkan kelas berikut:

open System

type Point1(angle,radius) =
    new() = Point1(angle=0.0, radius=0.0)
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
    member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
    static member Circle(n) =
        [ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]

Jenis F# yang disimpulkan dari kelas ini adalah sebagai berikut:

type Point1 =
    new : unit -> Point1
    new : angle:double * radius:double -> Point1
    static member Circle : n:int -> Point1 list
    member Stretch : l:double -> Point1
    member Warp : f:(double -> double) -> Point1
    member Angle : double
    member Radius : double

Mari kita lihat bagaimana jenis F# ini muncul ke programmer dengan menggunakan bahasa pemrogram .NET yang lain. Misalnya, perkiraan C# "tanda tangan" adalah sebagai berikut:

// C# signature for the unadjusted Point1 class
public class Point1
{
    public Point1();

    public Point1(double angle, double radius);

    public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);

    public Point1 Stretch(double factor);

    public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

Ada beberapa poin penting yang perlu diperhatikan tentang cara F# mewakili konstruksi di sini. Misalnya:

  • Metadata seperti nama argumen telah dipertahankan.

  • Metode F# yang mengambil dua argumen menjadi metode C# yang mengambil dua argumen.

  • Fungsi dan daftar menjadi referensi ke jenis terkait di dalam pustaka F#.

Kode berikut menunjukkan cara menyesuaikan kode ini untuk mempertimbangkan berbagai hal-hal ini.

namespace SuperDuperFSharpLibrary.Types

type RadialPoint(angle:double, radius:double) =

    /// Return a point at the origin
    new() = RadialPoint(angle=0.0, radius=0.0)

    /// The angle to the point, from the x-axis
    member x.Angle = angle

    /// The distance to the point, from the origin
    member x.Radius = radius

    /// Return a new point, with radius multiplied by the given factor
    member x.Stretch(factor) =
        RadialPoint(angle=angle, radius=radius * factor)

    /// Return a new point, with angle transformed by the function
    member x.Warp(transform:Func<_,_>) =
        RadialPoint(angle=transform.Invoke angle, radius=radius)

    /// Return a sequence of points describing an approximate circle using
    /// the given count of points
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }

Jenis F# yang disimpulkan dari kode ini adalah sebagai berikut:

type RadialPoint =
    new : unit -> RadialPoint
    new : angle:double * radius:double -> RadialPoint
    static member Circle : count:int -> seq<RadialPoint>
    member Stretch : factor:double -> RadialPoint
    member Warp : transform:System.Func<double,double> -> RadialPoint
    member Angle : double
    member Radius : double

Tanda tangan C# sekarang adalah sebagai berikut:

public class RadialPoint
{
    public RadialPoint();

    public RadialPoint(double angle, double radius);

    public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);

    public RadialPoint Stretch(double factor);

    public RadialPoint Warp(System.Func<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

Perbaikan yang dilakukan untuk menyiapkan jenis ini agar digunakan sebagai bagian dari pustaka vanilla .NET adalah sebagai berikut:

  • Disesuaikan beberapa nama: Point1, n, l, dan f masing-masing menjadi RadialPoint, count, factor, dan transform.

  • Gunakan jenis pengembalian seq<RadialPoint> alih-alih RadialPoint list dengan mengubah konstruksi daftar menggunakan [ ... ] ke konstruksi urutan menggunakan IEnumerable<RadialPoint>.

  • Gunakan jenis delegasi .NET System.Func alih-alih jenis fungsi F#.

Ini membuatnya jauh lebih baik untuk dipakai dalam kode C#.