Bisakah Anda bayangkan ini adalah kesepuluh siklus! Meskipun narasi sebelumnya berfokus pada gaya yang murni fungsional, kadang-kadang nyaman untuk beralih ke gaya berorientasi objek. Dan salah satu fitur utama gaya berorientasi objek adalah kemampuan untuk melampirkan fungsi ke kelas dan mengakses kelas melalui titik untuk mendapatkan perilaku yang diinginkan.

Dalam F #, ini dimungkinkan dengan fitur yang disebut "ketik ekstensi". Jenis F # apa pun, bukan hanya kelas, dapat memiliki fungsi terlampir.
Berikut adalah contoh melampirkan fungsi ke tipe rekaman.
module Person = type T = {First:string; Last:string} with // -, member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName
Poin-poin penting untuk diperhatikan:
- Kata kunci
with
menunjukkan awal daftar anggota. - Kata kunci
member
menunjukkan bahwa fungsinya adalah anggota (yaitu metode) - Kata
this
adalah label dari objek yang disebut metode ini (juga disebut "pengenal diri"). Kata ini adalah awalan dari nama fungsi, dan di dalam fungsi Anda dapat menggunakannya untuk merujuk ke instance saat ini. Tidak ada persyaratan untuk kata-kata yang digunakan sebagai pengenal diri, itu cukup bahwa mereka stabil. Anda dapat menggunakan this
, self
, me
atau kata lain yang biasanya digunakan sebagai referensi untuk diri sendiri.
Tidak perlu menambahkan anggota bersama dengan deklarasi tipe, Anda selalu dapat menambahkannya nanti dalam modul yang sama:
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} // , type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName
Contoh-contoh ini menunjukkan panggilan ke "ekstensi intrinsik". Mereka dikompilasi menjadi suatu tipe dan akan tersedia dimanapun tipe tersebut digunakan. Mereka juga akan ditampilkan saat menggunakan refleksi.
Ekstensi internal bahkan memungkinkan Anda untuk membagi definisi tipe menjadi beberapa file, selama semua komponen menggunakan namespace yang sama dan kompilasi menjadi satu rakitan. Seperti kelas parsial dalam C #, ini dapat berguna untuk memisahkan kode yang dihasilkan dan ditulis tangan.
Ekstensi opsional
Alternatifnya adalah menambahkan anggota tambahan dari modul yang sama sekali berbeda. Itu disebut "ekstensi opsional." Mereka tidak dikompilasi di dalam kelas, dan memerlukan modul lingkup yang berbeda untuk bekerja dengannya (perilaku ini menyerupai metode ekstensi dari C #).
Misalnya, biarkan tipe Person
didefinisikan:
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} // , type T with member this.SortableName = this.Last + ", " + this.First
Contoh di bawah ini menunjukkan cara menambahkan ekstensi UppercaseName
ke dalam modul lain:
// module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper()
Sekarang Anda dapat mencoba ekstensi ini:
let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName
Ups, kami mendapat kesalahan. Itu terjadi karena PersonExtensions
tidak dalam ruang lingkup. Seperti dalam C #, untuk menggunakan ekstensi apa pun, Anda harus memasukkannya dalam ruang lingkup.
Setelah kami melakukan ini, itu akan berhasil:
// ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName
Ekstensi Jenis Sistem
Anda juga dapat memperluas jenis dari .NET libraries. Tetapi harus diingat bahwa ketika memperluas jenis, Anda harus menggunakan nama aslinya, dan bukan alias.
Misalnya, jika Anda mencoba untuk memperluas int
, tidak akan ada yang berhasil, karena int
bukan nama yang valid untuk jenis:
type int with member this.IsEven = this % 2 = 0
Sebagai gantinya, gunakan System.Int32
:
type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i
Anggota statis
Anda dapat membuat fungsi anggota statis menggunakan:
- tambahkan
static
- hapus tag
this
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName
Anda dapat membuat anggota statis untuk tipe sistem:
type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi
Melampirkan fitur yang ada
Pola yang sangat umum adalah perlekatan fungsi independen yang ada ke suatu jenis. Ini memberikan beberapa keuntungan:
- Selama pengembangan, Anda dapat mendeklarasikan fungsi independen yang merujuk fungsi independen lainnya. Ini akan menyederhanakan pengembangan, karena inferensi tipe bekerja jauh lebih baik dengan gaya fungsional daripada dengan yang berorientasi objek ("point to point").
- Tetapi beberapa fungsi utama dapat dilampirkan ke suatu jenis. Ini memungkinkan pengguna untuk memilih gaya mana yang akan digunakan - fungsional atau berorientasi objek.
Contoh dari solusi tersebut adalah fungsi dari perpustakaan F #, yang menghitung panjang daftar. Anda dapat menggunakan fungsi independen dari modul List
atau menyebutnya sebagai metode instan.
let list = [1..10] // let len1 = List.length list // - let len2 = list.Length
Dalam contoh berikut, tipe awalnya tidak memiliki anggota, kemudian beberapa fungsi didefinisikan, dan akhirnya fungsi fullName
dilampirkan ke tipe.
module Person = // , type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let fullName {First=first; Last=last} = first + " " + last // type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person // let fullname2 = person.FullName //
Fungsi fullName
memiliki satu parameter, person
. Anggota terlampir menerima parameter dari tautan-sendiri.
Menambahkan Fungsi yang Ada dengan Beberapa Parameter
Ada fitur bagus lainnya. Jika suatu fungsi yang didefinisikan sebelumnya mengambil beberapa parameter, maka ketika Anda melampirkannya ke suatu jenis, Anda tidak perlu membuat daftar semua parameter ini lagi. Cukup menentukan parameter this
terlebih dahulu.
Pada contoh di bawah ini, fungsi hasSameFirstAndLastName
memiliki tiga parameter. Namun, ketika melampirkan sudah cukup untuk menyebutkan hanya satu!
module Person = // type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast // type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" // let result2 = person.HasSameFirstAndLastName "bob" "smith" //
Mengapa ini berhasil? Petunjuk: pikirkan tentang currying dan penggunaan parsial!
Metode Tuple
Saat kami memiliki metode dengan lebih dari satu parameter, Anda harus membuat keputusan:
- kita dapat menggunakan formulir standar (curried), di mana parameter dipisahkan oleh spasi, dan sebagian aplikasi didukung.
- atau kita bisa melewatkan semua parameter sekaligus dalam bentuk tuple yang dipisahkan oleh koma.
Bentuk kari lebih fungsional, sedangkan bentuk tuple lebih berorientasi objek.
Formulir tuple juga digunakan untuk berinteraksi dengan F # dengan pustaka .NET standar, jadi Anda harus mempertimbangkan pendekatan ini secara lebih rinci.
Situs pengujian kami akan menjadi tipe Product
dengan dua metode, yang masing-masing diimplementasikan oleh salah satu metode yang dijelaskan di atas. Metode CurriedTotal
dan TupleTotal
melakukan hal yang sama: mereka menghitung total biaya produk untuk jumlah dan diskon tertentu.
type Product = {SKU:string; Price: float} with // member this.CurriedTotal qty discount = (this.Price * float qty) - discount // member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount
Kode uji:
let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0)
Sejauh ini tidak ada banyak perbedaan.
Tetapi kita tahu bahwa versi kari dapat diterapkan sebagian:
let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10
Di sisi lain, versi tuple mampu melakukan sesuatu yang tidak dapat digulung, yaitu:
- Parameter yang Dinamai
- Parameter opsional
- Kelebihan
Dinamai parameter dengan parameter dalam bentuk tuple
Pendekatan tuple mendukung parameter bernama:
let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10)
Seperti yang Anda lihat, ini memungkinkan Anda untuk mengubah urutan argumen dengan secara eksplisit menyebutkan nama.
Perhatian: jika hanya beberapa parameter yang memiliki nama, maka parameter ini harus selalu di akhir.
Parameter opsional dengan parameter dalam bentuk tuple
Untuk metode dengan parameter dalam bentuk tuple, Anda dapat menandai parameter sebagai opsional menggunakan awalan dalam bentuk tanda tanya di depan nama parameter.
- Jika parameter diatur, maka
Some value
akan diteruskan ke fungsi - Kalau tidak, tidak
None
akan datang
Contoh:
type Product = {SKU:string; Price: float} with // member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount
Dan tesnya:
let product = {SKU="ABC"; Price=2.0} // let total1 = product.TupleTotal2(10) // let total2 = product.TupleTotal2(10,1.0)
Memeriksa secara eksplisit untuk None
dan Some
dapat membosankan, tetapi ada solusi yang lebih elegan untuk menangani parameter opsional.
Ada fungsi defaultArg
yang menggunakan nama parameter sebagai argumen pertama dan nilai default sebagai argumen kedua. Jika parameter diatur, nilai yang sesuai akan dikembalikan, jika tidak, nilai default.
Kode yang sama menggunakan defaulArg
:
type Product = {SKU:string; Price: float} with // member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount
Metode kelebihan beban
Di C #, Anda dapat membuat beberapa metode dengan nama yang sama yang berbeda dalam tanda tangannya (misalnya, berbagai jenis parameter dan / atau jumlahnya).
Dalam model yang murni fungsional, ini tidak masuk akal - fungsinya bekerja dengan tipe argumen tertentu (domain) dan tipe nilai pengembalian spesifik (rentang). Fungsi yang sama tidak dapat berinteraksi dengan domain dan rentang lain.
Namun, F # mendukung metode overloading, tetapi hanya untuk metode (yang terlampir pada jenis) dan hanya yang ditulis dalam gaya tuple.
Ini adalah contoh dengan variasi lain dari metode TupleTotal
!
type Product = {SKU:string; Price: float} with // member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty // member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount
Sebagai aturan, kompiler F # bersumpah bahwa ada dua metode dengan nama yang sama, tetapi dalam kasus ini ini dapat diterima, karena mereka dinyatakan dalam notasi tuple dan tanda tangan mereka berbeda. (Untuk memperjelas metode mana yang dipanggil, saya menambahkan pesan kecil untuk debugging)
Contoh penggunaan:
let product = {SKU="ABC"; Price=2.0} // let total1 = product.TupleTotal3(10) // let total2 = product.TupleTotal3(10,1.0)
Hai! Tidak begitu cepat ... Kerugian menggunakan metode
Berasal dari dunia berorientasi objek, Anda dapat tergoda untuk menggunakan metode di mana saja, karena itu adalah sesuatu yang akrab. Tetapi Anda harus berhati-hati, karena mereka memiliki sejumlah kelemahan serius:
- Metode tidak bekerja dengan baik dengan inferensi tipe
- Metode tidak berfungsi dengan baik dengan fungsi tingkat tinggi
Bahkan, dengan menyalahgunakan metode, Anda dapat kehilangan aspek pemrograman yang paling kuat dan berguna di F #.
Mari kita lihat apa yang saya maksud.
Metode berinteraksi buruk dengan inferensi tipe
Mari kita kembali ke contoh dengan Person
, di mana logika yang sama diimplementasikan dalam fungsi independen dan metode:
module Person = // type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this
Sekarang mari kita lihat seberapa baik tipe inferensi bekerja dengan masing-masing metode. Misalkan saya ingin mencetak nama lengkap seseorang, maka saya akan mendefinisikan fungsi printFullName
, yang printFullName
person
sebagai parameter.
Kode menggunakan fungsi independen dari modul:
open Person // let printFullName person = printfn "Name is %s" (fullName person) // // val printFullName : Person.T -> unit
Ini mengkompilasi tanpa masalah, dan ketik inferensi dengan benar mengidentifikasi parameter sebagai Person
.
Sekarang coba versi melalui titik:
open Person // " " let printFullName2 person = printfn "Name is %s" (person.FullName)
Kode ini tidak dikompilasi sama sekali, karena type inference tidak memiliki informasi yang cukup untuk menentukan tipe parameter. Objek apa pun dapat mengimplementasikan .FullName
- ini tidak cukup untuk output.
Ya, kami dapat membuat anotasi fungsi dengan tipe parameter, tetapi karena ini, seluruh titik inferensi tipe otomatis hilang.
Metode berjalan buruk dengan fungsi tingkat tinggi
Masalah serupa muncul dalam fungsi tingkat tinggi. Misalnya, ada daftar orang, dan kita perlu mendapatkan daftar nama lengkap mereka.
Dalam kasus fungsi independen, solusinya sepele:
open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] // list |> List.map fullName
Dalam hal metode objek, Anda harus membuat lambda khusus di mana-mana:
open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] // list |> List.map (fun p -> p.FullName)
Tetapi ini masih merupakan contoh yang cukup sederhana. Metode objek cukup dapat diterima untuk komposisi, tidak nyaman dalam pipa, dll.
Karena itu, jika Anda baru mengenal pemrograman fungsional, maka saya mendorong Anda: jika Anda bisa, jangan gunakan metode, terutama dalam proses pembelajaran. Mereka akan menjadi penopang yang tidak akan memungkinkan Anda untuk mengekstrak manfaat maksimal dari pemrograman fungsional.
Sumber Daya Tambahan
Ada banyak tutorial untuk F #, termasuk materi untuk mereka yang datang dengan pengalaman C # atau Java. Tautan berikut mungkin berguna saat Anda masuk lebih dalam ke F #:
Beberapa cara lain untuk mulai belajar F # juga dijelaskan.
Akhirnya, komunitas F # sangat ramah pemula. Ada obrolan yang sangat aktif di Slack, didukung oleh F # Software Foundation, dengan kamar pemula yang dapat Anda gabung dengan bebas . Kami sangat menyarankan Anda melakukan ini!
Jangan lupa untuk mengunjungi situs komunitas berbahasa Rusia F # ! Jika Anda memiliki pertanyaan tentang belajar bahasa, dengan senang hati kami akan membahasnya di ruang obrolan:
Tentang penulis terjemahan
Diterjemahkan oleh @kleidemos
Perubahan terjemahan dan editorial dilakukan oleh upaya komunitas pengembang F # berbahasa Rusia . Kami juga berterima kasih kepada @schvepsss dan @shwars karena telah menyiapkan artikel ini untuk dipublikasikan.