Ini sudah merupakan bagian 9 dari serangkaian artikel tentang pemrograman fungsional dalam F #! Saya yakin bahwa di Habré tidak banyak siklus yang panjang. Tapi kita tidak akan berhenti. Hari ini kita akan berbicara tentang fungsi bersarang, modul, ruang nama, dan pencampuran tipe dan fungsi dalam modul.

Sekarang Anda tahu cara mendefinisikan fungsi, tetapi bagaimana mengaturnya?
F # memiliki tiga opsi:
- fungsi dapat disarangkan di fungsi lainnya.
- pada level aplikasi, fungsi level atas dikelompokkan menjadi “modul”.
- atau Anda dapat mengikuti pendekatan berorientasi objek dan melampirkan fungsi ke tipe sebagai metode.
Dalam artikel ini, kami akan mempertimbangkan dua metode pertama, dan yang tersisa di metode berikutnya.
Fungsi Bersarang
Di F #, Anda dapat menentukan fungsi di dalam fungsi lain. Ini adalah cara yang baik untuk merangkum fungsi tambahan yang hanya diperlukan untuk fungsi utama dan tidak boleh terlihat dari luar.
Pada contoh di bawah ini, add
bersarang di addThreeNumbers
:
let addThreeNumbers xyz = // let add n = fun x -> x + n // x |> add y |> add z addThreeNumbers 2 3 4
Fungsi bersarang dapat mengakses parameter induk secara langsung, karena berada dalam cakupannya.
Jadi, dalam contoh di bawah ini, fungsi bersarang printError
tidak perlu parameter, karena dia dapat mengakses dan max
secara langsung.
let validateSize max n = // let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max // if n > max then printError() validateSize 10 9 validateSize 10 11
Pola yang sangat umum adalah fungsi utama yang mendefinisikan fungsi pembantu rekursif bersarang, yang disebut dengan nilai awal yang sesuai.
Berikut ini adalah contoh kode tersebut:
let sumNumbersUpTo max = // let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) // recursiveSum max 0 sumNumbersUpTo 10
Cobalah untuk menghindari sarang yang dalam, terutama dalam kasus akses langsung (bukan dalam bentuk parameter) ke variabel induk.
Fungsi bersarang terlalu dalam akan sulit untuk dipahami sebagai yang terburuk dari banyak cabang imperatif bersarang.
Contoh cara tidak melakukan:
// wtf, ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x
Modul
Modul hanyalah kumpulan fungsi yang dikelompokkan bersama, biasanya karena mereka bekerja dengan tipe data yang sama.
Definisi modul sangat mirip dengan definisi fungsi. Dimulai dengan kata kunci module
, kemudian muncul tanda =
, diikuti oleh isi modul.
Isi modul harus diformat dengan offset, serta ekspresi dalam definisi fungsi.
Definisi modul yang berisi dua fungsi:
module MathStuff = let add xy = x + y let subtract xy = x - y
Jika Anda membuka kode ini di Visual Studio, maka ketika Anda mengarahkan kursor ke add
Anda bisa melihat nama lengkap add
, yang sebenarnya adalah MathStuff.add
, seolah-olah MastStuff
adalah kelas, dan add
adalah metode.
Sebenarnya, inilah yang sebenarnya terjadi. Di belakang layar, kompiler F # menciptakan kelas statis dengan metode statis. Setara C # akan terlihat seperti ini:
static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } }
Mengakui bahwa modul hanyalah kelas statis dan fungsi adalah metode statis akan memberikan pemahaman yang baik tentang bagaimana modul bekerja di F #, karena sebagian besar aturan yang berlaku untuk kelas statis juga berlaku untuk modul.
Dan seperti dalam C #, setiap fungsi yang berdiri sendiri harus menjadi bagian dari kelas, dalam F #, setiap fungsi yang berdiri sendiri harus menjadi bagian dari modul.
Akses ke fungsi di luar modul
Jika Anda perlu mengakses suatu fungsi dari modul lain, Anda dapat merujuknya melalui nama lengkapnya.
module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = // MathStuff let add1 x = MathStuff.add x 1
Anda juga dapat mengimpor semua fungsi modul lain menggunakan arahan open
, setelah itu Anda dapat menggunakan nama pendek dan bukan nama lengkap.
module OtherStuff = open MathStuff // let add1 x = add x 1
Aturan untuk menggunakan nama sangat diharapkan. Anda selalu dapat mengakses fungsi dengan nama lengkapnya, atau Anda dapat menggunakan nama relatif atau tidak lengkap tergantung pada ruang lingkup saat ini.
Modul Bersarang
Seperti kelas statis, modul dapat berisi modul bersarang:
module MathStuff = let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Modul lain dapat merujuk ke fungsi dalam modul bersarang menggunakan nama lengkap atau relatif, yang sesuai:
module OtherStuff = open MathStuff let add1 x = add x 1 // let add1Float x = MathStuff.FloatLib.add x 1.0 // let sub1Float x = FloatLib.subtract x 1.0
Modul tingkat atas
Dengan demikian, karena modul dapat disarangkan, oleh karena itu, naik rantai, Anda dapat mencapai beberapa modul induk dari tingkat atas. Memang benar.
Modul tingkat atas didefinisikan secara berbeda, tidak seperti modul yang ditunjukkan sebelumnya.
- Baris
module MyModuleName
harus merupakan deklarasi pertama dalam file - Tanda
=
hilang - Konten modul tidak boleh diindentasi
Secara umum, deklarasi "tingkat atas" harus ada di setiap file .FS
sumber. Ada beberapa pengecualian, tetapi ini masih merupakan praktik yang baik. Nama modul tidak harus cocok dengan nama file, tetapi dua file tidak dapat berisi modul dengan nama yang sama.
Untuk file .FSX
, deklarasi modul tidak diperlukan, dalam hal ini nama file skrip secara otomatis menjadi nama modul.
Contoh dari MathStuff
dideklarasikan sebagai modul "modul teratas":
// module MathStuff let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Perhatikan bahwa tidak ada lekukan dalam kode "tingkat atas" ( module MathStuff
), sedangkan isi modul FloatLib
bersarang masih harus diindentasi.
Isi modul lainnya
Selain fungsi, modul dapat berisi deklarasi lain, seperti deklarasi tipe, nilai sederhana, dan kode inisialisasi (misalnya, konstruktor statis)
module MathStuff = // let add xy = x + y let subtract xy = x - y // type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg // / do printfn "module initialized"
Ngomong-ngomong, jika Anda menjalankan contoh-contoh ini secara interaktif, Anda mungkin perlu memulai kembali sesi cukup sering sehingga kode tetap "segar" dan tidak terinfeksi oleh perhitungan sebelumnya.
Penyembunyian (Tumpang tindih, Membayangi)
Ini lagi modul sampel kami. Perhatikan bahwa MathStuff
berisi fungsi add
serta FloatLib
.
module MathStuff = let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Apa yang terjadi jika Anda membuka kedua modul dalam ruang lingkup saat ini dan panggilan add
?
open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int
Dan kebetulan modul MathStuff.FloatLib
mendefinisikan kembali MathStuff
asli, yang diblokir (disembunyikan) oleh modul FloatLib
.
Akibatnya, kami mendapatkan kesalahan kompiler FS0001, karena parameter pertama 1
diharapkan sebagai float. Untuk memperbaikinya, Anda harus mengubah 1
ke 1.0
.
Sayangnya, dalam praktiknya ini diam - diam dan mudah diabaikan. Terkadang menggunakan teknik ini Anda dapat melakukan trik-trik menarik, hampir seperti subclass, tetapi paling sering keberadaan fungsi dengan nama yang sama mengganggu (misalnya, dalam kasus fungsi map
sangat umum).
Jika Anda ingin menghindari perilaku ini, ada cara untuk menghentikannya dengan atribut RequireQualifiedAccess
. Contoh yang sama di mana kedua modul didekorasi dengan atribut ini:
[<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y // [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Sekarang arahan open
tidak tersedia:
open MathStuff // open MathStuff.FloatLib //
Tetapi Anda masih dapat mengakses fungsi (tanpa ambiguitas) melalui nama lengkapnya:
let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0
Kontrol akses
F # mendukung penggunaan operator kontrol akses .NET standar seperti public
, private
dan internal
. Artikel MSDN berisi informasi lengkap.
- Penentu akses ini dapat diterapkan pada fungsi, nilai, tipe, dan deklarasi tingkat atas ("biarkan terikat") lainnya dalam modul. Mereka juga dapat ditentukan untuk modul itu sendiri (misalnya, modul bersarang pribadi mungkin diperlukan).
- Secara default, semuanya memiliki akses publik (dengan pengecualian beberapa kasus), jadi untuk melindunginya Anda harus menggunakan
private
atau internal
.
Penentu akses ini hanyalah salah satu cara untuk mengontrol visibilitas di F #. Cara yang sama sekali berbeda adalah dengan menggunakan file tanda tangan yang menyerupai file header C. Mereka secara abstrak menggambarkan isi modul. Tanda tangan sangat berguna untuk enkapsulasi serius, tetapi untuk mempertimbangkan kemampuannya Anda harus menunggu seri yang direncanakan tentang enkapsulasi dan keamanan berdasarkan pada kemampuan .
Ruang nama
Ruang nama di F # mirip dengan ruang nama dari C #. Mereka dapat digunakan untuk mengatur modul dan tipe untuk menghindari konflik nama.
Namespace dideklarasikan menggunakan kata kunci namespace
:
namespace Utilities module MathStuff = // let add xy = x + y let subtract xy = x - y
Karena namespace ini, nama lengkap modul MathStuff
menjadi Utilities.MathStuff
, dan nama lengkapnya adalah Utilities.MathStuff.add
.
Aturan indentasi yang sama berlaku untuk modul dalam ruang nama yang ditunjukkan di atas untuk modul.
Anda juga dapat mendeklarasikan namespace secara eksplisit dengan menambahkan periode pada nama modul. Yaitu Kode di atas dapat ditulis ulang seperti ini:
module Utilities.MathStuff // let add xy = x + y let subtract xy = x - y
Nama lengkap modul MathStuff
masih Utilities.MathStuff
, tetapi sekarang ini adalah modul tingkat atas dan isinya tidak perlu lekukan.
Beberapa fitur tambahan untuk menggunakan ruang nama:
- Ruang nama adalah opsional untuk modul. Tidak seperti C #, untuk proyek F # tidak ada namespace default, jadi modul tingkat atas tanpa namespace akan bersifat global. Jika Anda berencana membuat pustaka yang dapat digunakan kembali, Anda harus menambahkan beberapa ruang nama untuk menghindari konflik dengan kode pustaka lain.
- Namespaces dapat secara langsung mengandung deklarasi tipe, tetapi bukan deklarasi fungsi. Seperti disebutkan sebelumnya, semua deklarasi fungsi dan nilai harus menjadi bagian dari modul.
- Akhirnya, ingatlah bahwa ruang nama tidak berfungsi dalam skrip. Misalnya, jika Anda mencoba mengirim deklarasi namespace, seperti
namespace Utilities
, ke jendela interaktif, kesalahan diterima.
Hirarki Namespace
Anda dapat membangun hierarki ruang nama dengan hanya membagi nama dengan titik-titik:
namespace Core.Utilities module MathStuff = let add xy = x + y
Anda juga dapat mendeklarasikan dua ruang nama dalam satu file jika Anda mau. Perlu dicatat bahwa semua ruang nama harus dideklarasikan dengan nama lengkap mereka - mereka tidak mendukung bersarang.
namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y
Konflik nama antara namespace dan modul tidak dimungkinkan.
namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core // - Core.Utilities // ! module Utilities = let add xy = x + y
Mencampur jenis dan fungsi dalam modul
Seperti yang telah kita lihat, modul biasanya terdiri dari banyak fungsi yang saling tergantung yang berinteraksi dengan tipe data tertentu.
Dalam OOP, struktur data dan fungsi di atasnya akan digabungkan bersama dalam kelas. Dan dalam F # fungsional, struktur data dan fungsi di atas mereka digabungkan menjadi sebuah modul.
Ada dua pola untuk menggabungkan jenis dan fungsi bersama:
- tipe dideklarasikan secara terpisah dari fungsi
- tipe dideklarasikan dalam modul yang sama dengan fungsi
Dalam kasus pertama, tipe dideklarasikan di luar modul apa pun (tetapi di namespace), setelah itu fungsi yang bekerja dengan tipe ini ditempatkan dalam modul dengan tipe yang sama.
// namespace Example // type PersonType = {First:string; Last:string} // , module Person = // let create first last = {First=first; Last=last} // , let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s"
Atau, jenisnya dideklarasikan di dalam modul dan memiliki nama sederhana seperti " T
" atau nama modul. Akses ke fungsi kira-kira sebagai berikut: MyModule.Func
dan MyModule.Func2
, dan akses ke jenis: MyModule.T
:
module Customer = // Customer.T - type T = {AccountId:int; Name:string} // let create id name = {T.AccountId=id; T.Name=name} // , let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b"
Perhatikan bahwa dalam kedua kasus harus ada fungsi konstruktor yang membuat instance baru dari jenis (pabrik). Kemudian, dalam kode klien, Anda tidak perlu mengakses nama jenis secara eksplisit, dan Anda tidak perlu bertanya-tanya apakah jenisnya ada di dalam modul atau tidak.
Jadi, cara mana yang harus dipilih?
- Pendekatan pertama lebih seperti .NET klasik, dan harus lebih disukai jika Anda berencana untuk menggunakan perpustakaan ini untuk kode di luar F #, di mana kelas yang ada secara terpisah diharapkan.
- Pendekatan kedua lebih umum dalam bahasa fungsional lainnya. Jenis di dalam modul dikompilasi sebagai kelas bersarang, yang biasanya tidak nyaman untuk bahasa OOP.
Untuk diri sendiri, Anda dapat bereksperimen dengan kedua metode ini. Dalam hal pengembangan tim, satu gaya harus dipilih.
Modul yang hanya berisi tipe
Jika ada banyak jenis yang perlu dideklarasikan tanpa fungsi apa pun, jangan repot-repot menggunakan modul. Anda bisa mendeklarasikan tipe secara langsung di namespace tanpa menggunakan kelas bersarang.
Misalnya, Anda mungkin ingin melakukan ini:
// module Example // type PersonType = {First:string; Last:string} // , ...
Dan di sini ada cara lain untuk melakukan hal yang sama. module
kata hanya diganti dengan kata namespace
.
// namespace Example // type PersonType = {First:string; Last:string}
Dalam kedua kasus, PersonType
akan memiliki nama lengkap yang sama.
Harap dicatat bahwa penggantian ini hanya berfungsi dengan jenis. Fungsi harus selalu dinyatakan di dalam modul.
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.