Bagian ketiga dari serangkaian artikel tentang pemrograman fungsional telah berhenti. Hari ini kita akan berbicara tentang semua jenis paradigma ini dan menunjukkan contoh penggunaannya. Informasi lebih lanjut tentang tipe primitif, tipe umum, dan banyak lagi yang lainnya!

Sekarang setelah kita memahami beberapa fungsi, kita akan melihat bagaimana tipe berinteraksi dengan fungsi seperti domain dan rentang. Artikel ini hanya review. Untuk perendaman lebih dalam dalam jenis ada serangkaian "memahami tipe F #" .
Untuk memulai, kita perlu sedikit pemahaman yang lebih baik tentang jenis notasi. Kami melihat notasi panah " ->
" yang memisahkan domain dan jangkauan. Jadi tanda tangan fungsi selalu terlihat seperti ini:
val functionName : domain -> range
Beberapa contoh fungsi lainnya:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
Jika Anda menjalankan kode ini di jendela interaktif , Anda dapat melihat tanda tangan berikut:
val intToString : int -> string val stringToInt : string -> int
Artinya:
intToString
memiliki domain tipe int
, yang memetakan ke berbagai jenis string
.stringToInt
memiliki domain tipe string
, yang memetakan ke berbagai tipe int
.
Tipe primitif
Ada tipe primitif yang diharapkan: string, int, float, bool, char, byte, dll., Serta banyak turunan lainnya dari sistem tipe .NET.
Beberapa lagi contoh fungsi dengan tipe primitif:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 2 let stringToString x = x + " world"
dan tanda tangan mereka:
val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string
Ketikkan Anotasi
Dalam contoh-contoh sebelumnya, kompiler F # dengan benar menentukan jenis parameter dan hasil. Tetapi ini tidak selalu terjadi. Jika Anda mencoba menjalankan kode berikut, Anda akan mendapatkan kesalahan kompilasi:
let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type
Kompiler tidak mengetahui tipe argumen "x", dan karena itu, ia tidak tahu apakah "Panjang" adalah metode yang valid. Dalam kebanyakan kasus, ini dapat diperbaiki dengan meneruskan "ketik anotasi" ke kompiler F #. Kemudian dia akan tahu tipe mana yang digunakan. Dalam versi tetap, kami menunjukkan bahwa tipe "x" adalah string.
let stringLength (x:string) = x.Length
Kawat gigi di sekitar parameter x:string
itu penting. Jika dilewati, kompiler akan memutuskan bahwa string adalah nilai balik! Yaitu, titik dua terbuka digunakan untuk menunjukkan tipe nilai pengembalian, seperti yang ditunjukkan pada contoh berikut.
let stringLengthAsInt (x:string) :int = x.Length
Kami menunjukkan bahwa parameter x
adalah string, dan nilai kembali adalah bilangan bulat.
Tipe Fungsi sebagai Parameter
Fungsi yang mengambil fungsi lain sebagai parameter atau mengembalikan fungsi disebut fungsi urutan lebih tinggi ( fungsi urutan lebih tinggi kadang disingkat menjadi HOF). Mereka digunakan sebagai abstraksi untuk mengatur perilaku umum mungkin. Jenis fungsi ini sangat umum di F #, kebanyakan perpustakaan standar menggunakannya.
Pertimbangkan fungsi evalWith5ThenAdd2
, yang mengambil fungsi sebagai parameter, dan kemudian menghitung fungsi ini dari 5 dan menambahkan 2 ke hasilnya:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
Tanda tangan dari fungsi ini terlihat seperti ini:
val evalWith5ThenAdd2 : (int -> int) -> int
Anda dapat melihat bahwa domain adalah (int->int)
dan jangkauannya adalah int
. Apa artinya ini? Ini berarti bahwa parameter input bukan nilai sederhana, tetapi fungsi dari banyak fungsi dari int
ke int
. Nilai output bukan fungsi, tetapi hanya sebuah int
.
Mari kita coba:
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
dan dapatkan:
val add1 : int -> int val it : int = 8
" add1
" adalah fungsi yang memetakan int
ke int
, seperti yang kita lihat dari tanda tangan. Ini adalah parameter yang valid untuk evalWith5ThenAdd2
, dan hasilnya adalah 8.
Ngomong-ngomong, kata khusus " it
" digunakan untuk menunjukkan nilai yang dihitung terakhir, dalam hal ini adalah hasil yang kami tunggu-tunggu. Ini bukan kata kunci, itu hanya konvensi penamaan.
Kasus lain:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
memberi:
val times3 : int -> int val it : int = 17
" times3
" juga merupakan fungsi yang memetakan int
ke int
, seperti yang dapat dilihat dari tanda tangan. Ini juga merupakan parameter yang valid untuk evalWith5ThenAdd2
. Hasil perhitungannya adalah 17.
Harap dicatat bahwa data input adalah tipe sensitif. Jika fungsi yang dilewati menggunakan float
, bukan int
, maka tidak ada yang berfungsi. Sebagai contoh, jika kita memiliki:
let times3float x = x * 3.0 // - (float->float) evalWith5ThenAdd2 times3float
Kompiler, ketika mencoba mengkompilasi, akan mengembalikan kesalahan:
error FS0001: Type mismatch. Expecting a int -> int but given a float -> float
melaporkan bahwa fungsi input harus berupa fungsi tipe int->int
.
Berfungsi sebagai Output
Fungsi nilai juga bisa merupakan hasil dari fungsi. Misalnya, fungsi berikut akan menghasilkan fungsi "penambah" yang akan menambah nilai input.
let adderGenerator numberToAdd = (+) numberToAdd
Tanda tangannya:
val adderGenerator : int -> (int -> int)
berarti generator mengambil int
dan menciptakan fungsi ("penambah") yang memetakan ints
ke ints
. Mari kita lihat cara kerjanya:
let add1 = adderGenerator 1 let add2 = adderGenerator 2
Dua fungsi adder dibuat. Yang pertama membuat fungsi yang menambahkan 1 ke input, yang kedua menambahkan 2. Perhatikan bahwa tanda tangan persis seperti yang kami harapkan.
val add1 : (int -> int) val add2 : (int -> int)
Sekarang Anda dapat menggunakan fungsi yang dihasilkan seperti biasa, mereka tidak berbeda dengan fungsi yang didefinisikan secara eksplisit:
add1 5 // val it : int = 6 add2 5 // val it : int = 7
Menggunakan anotasi jenis untuk membatasi jenis fungsi
Pada contoh pertama, kami melihat sebuah fungsi:
let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int
Dalam contoh ini, F # dapat menyimpulkan bahwa " fn
" mengubah int
menjadi int
, sehingga tanda tangannya akan menjadi int->int
.
Tapi apa tanda tangan "fn" dalam kasus berikut?
let evalWith5 fn = fn 5
Jelas bahwa " fn
" adalah jenis fungsi yang mengambil int
, tetapi apa yang dikembalikan? Kompiler tidak dapat menjawab pertanyaan ini. Dalam kasus seperti itu, jika menjadi perlu untuk menunjukkan jenis fungsi, Anda dapat menambahkan jenis anotasi untuk parameter fungsi, serta untuk tipe primitif.
let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5
Selain itu, Anda dapat menentukan jenis pengembalian.
let evalWith5AsString fn :string = fn 5
Karena fungsi utama mengembalikan string
, fungsi " fn
" juga dipaksa untuk mengembalikan string
. Dengan demikian, tidak perlu secara eksplisit menentukan jenis " fn
".
Ketik "unit"
Dalam proses pemrograman, kami terkadang ingin fungsi melakukan sesuatu tanpa mengembalikan apa pun. Pertimbangkan fungsi " printInt
". Fungsi ini benar-benar tidak menghasilkan apa-apa. Ini hanya mencetak string ke konsol sebagai efek samping dari eksekusi.
let printInt x = printf "x is %i" x //
Apa tanda tangannya?
val printInt : int -> unit
Apa itu " unit
"?
Bahkan jika fungsi tidak mengembalikan nilai, ia masih membutuhkan jangkauan. Tidak ada fungsi "void" di dunia matematika. Setiap fungsi harus mengembalikan sesuatu, karena fungsinya adalah pemetaan, dan pemetaan itu harus menampilkan sesuatu!

Jadi, dalam F #, fungsi seperti ini mengembalikan tipe khusus hasil yang disebut " unit
". Ini hanya berisi satu nilai, dilambangkan dengan " ()
". Anda mungkin berpikir bahwa unit
dan ()
adalah sesuatu seperti "void" dan "null" dari C #, masing-masing. Tetapi tidak seperti mereka, unit
adalah tipe nyata, dan ()
sebenarnya. Untuk memverifikasi ini, lakukan saja:
let whatIsThis = ()
Tanda tangan berikut akan diterima:
val whatIsThis : unit = ()
Yang menunjukkan bahwa label " whatIsThis
" adalah tipe unit
dan dikaitkan dengan nilai ()
.
Sekarang, kembali ke tanda tangan " printInt
", kita dapat memahami arti dari entri ini:
val printInt : int -> unit
Tanda tangan ini mengatakan bahwa printInt
memiliki domain int
, yang diterjemahkan menjadi sesuatu yang tidak menarik bagi kami.
Fungsi tanpa parameter
Sekarang kita mengerti unit
, dapatkah kita memprediksi kemunculannya dalam konteks yang berbeda? Sebagai contoh, cobalah untuk membuat fungsi yang dapat digunakan kembali "hello world". Karena tidak ada input atau output, kita dapat mengharapkan unit -> unit
tanda tangan unit -> unit
. Mari kita lihat:
let printHello = printf "hello world" //
Hasil:
hello world val printHello : unit = ()
Tidak seperti yang kami harapkan. "Hello world" langsung ditampilkan, dan hasilnya bukan fungsi, tetapi nilai sederhana dari unit tipe. Kita dapat mengatakan bahwa ini adalah nilai sederhana, karena, seperti yang kita lihat sebelumnya, ia memiliki tanda tangan dari formulir:
val aName: type = constant
Dalam contoh ini, kita melihat bahwa printHello
benar-benar nilai sederhana ()
. Ini bukan fungsi yang bisa kita panggil nanti.
Apa perbedaan antara printInt
dan printHello
? Dalam hal printInt
nilainya tidak dapat ditentukan sampai kita mengetahui nilai parameter x
, jadi definisi tersebut adalah fungsi. Dalam hal printHello
tidak ada parameter, sehingga sisi kanan dapat ditentukan di tempatnya. Dan itu sama dengan ()
dengan efek samping berupa output ke konsol.
Anda dapat membuat fungsi benar-benar dapat digunakan kembali tanpa parameter, memaksa definisi untuk memiliki argumen unit
:
let printHelloFn () = printf "hello world" //
Sekarang tanda tangannya sama dengan:
val printHelloFn : unit -> unit
dan untuk menyebutnya, kita harus meneruskan ()
sebagai parameter:
printHelloFn ()
Memperkuat tipe unit dengan fungsi abaikan
Dalam beberapa kasus, kompiler memerlukan tipe unit
dan komplain. Misalnya, kedua kasus berikut ini akan menyebabkan kesalahan kompiler:
do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello"
Untuk membantu dalam situasi ini, ada fungsi ignore
khusus yang mengambil apa saja dan mengembalikan unit
. Versi kode ini yang benar adalah:
do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello"
Jenis Generik
Dalam kebanyakan kasus, jika tipe parameter fungsi bisa tipe apa saja, kita perlu mengatakan sesuatu tentangnya. F # menggunakan .NET generics untuk situasi seperti itu.
Misalnya, fungsi berikut mengonversi parameter ke string dengan menambahkan beberapa teks:
let onAStick x = x.ToString() + " on a stick"
Apa pun jenis parameternya, semua objek dapat dilakukan di ToString()
.
Tanda tangan:
val onAStick : 'a -> string
Apa tipe 'a
? Dalam F #, ini adalah cara untuk menunjukkan tipe generik yang tidak diketahui pada waktu kompilasi. Apostrof sebelum "a" berarti jenisnya generik. Setara dengan tanda tangan ini dalam C #:
string onAStick<a>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
Harus dipahami bahwa fungsi F # ini masih memiliki pengetikan yang kuat bahkan dengan tipe generik. Itu tidak menerima parameter tipe Object
. Pengetikan yang kuat itu baik karena memungkinkan Anda untuk menjaga keamanan tipenya saat menyusun fungsi.
Fungsi yang sama digunakan untuk int
, float
, dan string
.
onAStick 22 onAStick 3.14159 onAStick "hello"
Jika ada dua parameter umum, maka kompiler akan memberi mereka dua nama berbeda: 'a
untuk yang pertama, 'b
untuk yang kedua, dll. Sebagai contoh:
let concatString xy = x.ToString() + y.ToString()
Akan ada dua jenis generik dalam tanda tangan ini: 'a
dan 'b
:
val concatString : 'a -> 'b -> string
Di sisi lain, kompiler mengenali ketika hanya satu jenis generik diperlukan. Dalam contoh berikut, x
dan y
harus dari jenis yang sama:
let isEqual xy = (x=y)
Jadi, tanda tangan fungsi memiliki tipe generik yang sama untuk kedua parameter:
val isEqual : 'a -> 'a -> bool
Parameter umum juga sangat penting ketika datang ke daftar dan struktur abstrak lainnya, dan kita akan melihat banyak dari mereka dalam contoh berikut.
Jenis lainnya
Sejauh ini, hanya tipe dasar yang telah dibahas. Tipe-tipe ini dapat digabungkan dengan berbagai cara menjadi tipe yang lebih kompleks. Analisis penuh mereka nantinya akan di seri lain , tetapi sementara itu, dan di sini kita akan secara singkat menganalisis mereka, sehingga Anda dapat mengenalinya dalam tanda tangan fungsi.
- Tuples Ini adalah pasangan, rangkap tiga, dll., Terdiri dari jenis lain. Sebagai contoh,
("hello", 1)
adalah tuple berdasarkan string
dan int
. Tanda koma adalah ciri khas tupel, jika koma terlihat di suatu tempat dalam F #, ini hampir dijamin menjadi bagian dari tupel.
Dalam tanda tangan fungsi, tupel ditulis sebagai "produk" dari dua jenis yang terlibat. Dalam hal ini, tupel akan bertipe:
string * int // ("hello", 1)
- Koleksi Yang paling umum adalah daftar (daftar), seq (urutan) dan array. Daftar dan array berukuran tetap, sementara urutan berpotensi tak terbatas (di belakang layar, urutan adalah
IEnumrable
sama). Dalam tanda tangan fungsi, mereka memiliki kata kunci sendiri: " list
", " seq
" dan " []
" untuk array.
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- Opsi (tipe opsional) . Ini adalah pembungkus sederhana di atas objek yang mungkin hilang. Ada dua opsi:
Some
(ketika nilai ada) dan None
(ketika nilai tidak). Dalam tanda tangan fungsi, mereka memiliki kata kunci " option
" sendiri:
int option // Some 1
- Asosiasi yang ditandai (serikat terdiskriminasi) . Mereka dibangun dari banyak variasi tipe lain. Kami melihat beberapa contoh di "mengapa menggunakan F #?" . Dalam tanda tangan fungsi, mereka dirujuk berdasarkan nama tipe, mereka tidak memiliki kata kunci khusus.
- Jenis rekaman (catatan) . Jenis-jenis seperti struktur atau baris basis data, satu set nilai bernama. Kami juga melihat beberapa contoh di "mengapa menggunakan F #?" . Dalam tanda tangan fungsi, mereka dipanggil dengan nama tipe dan juga tidak memiliki kata kunci sendiri.
Uji pemahaman Anda tentang jenis
Berikut adalah beberapa ekspresi untuk menguji pemahaman Anda tentang tanda tangan fungsi. Untuk memeriksa, jalankan saja di jendela interaktif!
let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // : x? let testO x:string = x 1 // : :string ?
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.