Kami melanjutkan serangkaian artikel tentang pemrograman fungsional dalam F #. Hari ini kami memiliki topik yang sangat menarik: definisi fungsi. Termasuk, mari kita bicara tentang fungsi anonim, fungsi tanpa parameter, fungsi rekursif, kombinator, dan banyak lagi. Lihat di bawah kucing!

Definisi Fungsi
Kami sudah tahu cara membuat fungsi biasa menggunakan sintaks "let":
let add xy = x + y
Pada artikel ini, kita akan melihat beberapa cara lain untuk membuat fungsi, serta tips untuk mendefinisikannya.
Fungsi anonim (lambdas)
Jika Anda terbiasa dengan lambdas dalam bahasa lain, paragraf berikut akan tampak familier. Fungsi anonim (atau "ekspresi lambda") didefinisikan sebagai berikut:
fun parameter1 parameter2 etc -> expression
Dibandingkan dengan lambdas dari C #, ada dua perbedaan:
- lambdas harus dimulai dengan kata kunci yang
fun
, yang tidak diperlukan dalam C # - panah tunggal digunakan
->
, bukannya double =>
dari C #.
Definisi fungsi penambahan Lambda:
let add = fun xy -> x + y
Fungsi yang sama dalam bentuk tradisional:
let add xy = x + y
Lambdas sering digunakan dalam bentuk ekspresi kecil atau ketika tidak ada keinginan untuk mendefinisikan fungsi terpisah untuk ekspresi. Seperti yang telah Anda lihat, ketika bekerja dengan daftar ini tidak jarang.
// let add1 i = i + 1 [1..10] |> List.map add1 // [1..10] |> List.map (fun i -> i + 1)
Perhatikan bahwa tanda kurung harus digunakan di sekitar lambdas.
Lambdas juga digunakan ketika fungsi yang jelas berbeda diperlukan. Misalnya, " adderGenerator
" yang dibahas sebelumnya, yang telah kita bahas sebelumnya, dapat ditulis ulang menggunakan lambdas.
// let adderGenerator x = (+) x // let adderGenerator x = fun y -> x + y
Versi lambda sedikit lebih lama, tetapi segera menjelaskan bahwa fungsi peralihan akan dikembalikan.
Lambdas bisa disarangkan. Contoh lain dari definisi adderGenerator
, kali ini hanya pada lambdas.
let adderGenerator = fun x -> (fun y -> x + y)
Apakah Anda jelas bahwa ketiga definisi itu setara?
let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y)
Jika tidak, baca kembali bab tentang kari . Ini sangat penting untuk dipahami!
Pencocokan pola
Ketika suatu fungsi didefinisikan, dimungkinkan untuk mengirimkan parameter secara eksplisit, seperti pada contoh di atas, tetapi juga dimungkinkan untuk membandingkan dengan templat secara langsung di bagian parameter. Dengan kata lain, bagian parameter dapat berisi pola (pola yang cocok), dan bukan hanya pengidentifikasi!
Contoh berikut menunjukkan penggunaan pola dalam definisi fungsi:
type Name = {first:string; last:string} // let bob = {first="bob"; last="smith"} // // let f1 name = // let {first=f; last=l} = name // printfn "first=%s; last=%s" fl // let f2 {first=f; last=l} = // printfn "first=%s; last=%s" fl // f1 bob f2 bob
Jenis perbandingan ini hanya dapat terjadi ketika korespondensi selalu dapat ditentukan. Misalnya, Anda tidak dapat mencocokkan jenis dan daftar serikat dengan cara ini, karena beberapa kasus tidak dapat dicocokkan.
let f3 (x::xs) = // printfn "first element is=%A" x
Kompiler akan memberikan peringatan tentang pencocokan yang tidak lengkap (daftar kosong akan menyebabkan kesalahan dalam runtime di pintu masuk ke fungsi ini).
Kesalahan umum: tuple vs. banyak parameter
Jika Anda berasal dari bahasa mirip-C, tupel yang digunakan sebagai satu-satunya argumen fungsi dapat menyerupai fungsi multi-parameter. Tapi ini bukan hal yang sama! Seperti yang saya catat sebelumnya, jika Anda melihat koma, kemungkinan besar ini adalah tuple. Parameter dipisahkan oleh spasi.
Contoh kebingungan:
// let addTwoParams xy = x + y // - let addTuple aTuple = let (x,y) = aTuple x + y // // let addConfusingTuple (x,y) = x + y
- Definisi pertama, "
addTwoParams
", mengambil dua parameter, dipisahkan oleh spasi. - Definisi kedua, "
addTuple
", mengambil satu parameter. Parameter ini mengikat "x" dan "y" dari tuple dan menjumlahkannya. - Definisi ketiga, "
addConfusingTuple
", mengambil satu parameter seperti " addTuple
", tetapi triknya adalah bahwa tuple ini dibongkar (dicocokkan dengan pola) dan terikat sebagai bagian dari definisi parameter menggunakan pencocokan pola. Di belakang layar, semuanya terjadi persis sama seperti di addTuple
.
Mari kita lihat tanda tangan (selalu lihat jika Anda tidak yakin tentang sesuatu).
val addTwoParams : int -> int -> int // val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int
Dan sekarang di sini:
// addTwoParams 1 2 // ok -- addTwoParams (1,2) // error - // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b
Di sini kita melihat kesalahan dalam panggilan kedua.
Pertama, kompilator memperlakukan (1,2)
sebagai tuple umum dari formulir ('a * 'b)
, yang dicoba untuk diteruskan sebagai parameter pertama yang addTwoParams
. Setelah itu ia mengeluh bahwa parameter addTwoParams
diharapkan pertama tidak int
, tetapi upaya dilakukan untuk melewatkan sebuah tuple.
Untuk membuat tuple, gunakan koma!
addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 // , // ! addTuple y // ok addConfusingTuple y // ok
Dan sebaliknya, jika Anda melewati beberapa argumen ke fungsi menunggu tuple, Anda juga mendapatkan kesalahan yang tidak bisa dimengerti.
addConfusingTuple 1 2 // error -- // => error FS0003: This value is not a function and // cannot be applied
Kali ini, kompiler memutuskan bahwa setelah dua argumen addConfusingTuple
, addConfusingTuple
harus addConfusingTuple
. Dan entri " addConfusingTuple 1
" adalah aplikasi parsial dan harus mengembalikan fungsi perantara. Mencoba memanggil fungsi perantara ini dengan parameter "2" akan menimbulkan kesalahan, karena tidak ada fungsi antara! Kami melihat kesalahan yang sama seperti pada bab tentang currying, di mana kami membahas masalah dengan terlalu banyak parameter.
Mengapa tidak menggunakan tupel sebagai parameter?
Diskusi tupel di atas menunjukkan cara lain untuk mendefinisikan fungsi dengan banyak parameter: alih-alih melewatinya secara terpisah, semua parameter dapat dirakit menjadi satu struktur. Dalam contoh di bawah ini, fungsi tersebut mengambil parameter tunggal - tupel dari tiga elemen.
let f (x,y,z) = x + y * z // - int * int * int -> int // f (1,2,3)
Perlu dicatat bahwa tanda tangan berbeda dari tanda tangan suatu fungsi dengan tiga parameter. Hanya ada satu panah, satu parameter dan tanda bintang yang menunjuk ke tuple (int*int*int)
.
Kapan perlu mengirimkan argumen dengan parameter terpisah, dan kapan tuple?
- Ketika tuple signifikan dalam diri mereka sendiri. Misalnya, untuk operasi dalam ruang tiga dimensi, triple tuple akan lebih nyaman daripada tiga koordinat secara terpisah.
- Terkadang tupel digunakan untuk menggabungkan data yang harus disimpan bersama menjadi satu struktur. Misalnya, metode
TryParse
dari perpustakaan .NET mengembalikan hasil dan variabel Boolean sebagai tupel. Tetapi untuk menyimpan sejumlah besar data terkait, lebih baik untuk menentukan kelas atau catatan ( catatan .
Kasus Khusus: Tuple dan Fungsi .NET Library
Saat memanggil perpustakaan .NET, koma sangat umum!
Mereka semua menerima tupel, dan panggilannya terlihat sama seperti di C #:
// System.String.Compare("a","b") // System.String.Compare "a" "b"
Alasannya adalah bahwa fungsi .NET klasik tidak kari dan tidak dapat diterapkan sebagian. Semua parameter harus selalu ditransmisikan segera, dan cara yang paling jelas adalah menggunakan tuple.
Perhatikan bahwa panggilan ini hanya terlihat seperti mentransfer tupel, tetapi ini sebenarnya adalah kasus khusus. Anda tidak dapat memasukkan tupel asli ke fungsi-fungsi tersebut:
let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error
Jika Anda ingin menerapkan sebagian fungsi .NET, cukup tulis pembungkus di atasnya, seperti yang dilakukan sebelumnya , atau seperti yang ditunjukkan di bawah ini:
// let strCompare xy = System.String.Compare(x,y) // let strCompareWithB = strCompare "B" // ["A";"B";"C"] |> List.map strCompareWithB
Panduan untuk Memilih Parameter Individual dan Kelompok
Diskusi tentang tupel mengarah ke topik yang lebih umum: kapan parameter harus terpisah, dan kapan dikelompokkan?
Anda harus memperhatikan bagaimana F # berbeda dari C # dalam hal ini. Di C #, semua parameter selalu dilewati, jadi pertanyaan ini bahkan tidak muncul di sana! Karena aplikasi parsial dalam F #, hanya beberapa parameter yang dapat diwakili, sehingga perlu untuk membedakan antara kasus ketika parameter harus dikombinasikan dan kasus ketika mereka independen.
Rekomendasi umum tentang cara menyusun parameter saat merancang fungsi Anda sendiri.
- Dalam kasus umum, selalu lebih baik menggunakan parameter terpisah daripada melewati satu struktur, baik itu tupel atau catatan. Ini memungkinkan perilaku yang lebih fleksibel, seperti aplikasi parsial.
- Tetapi, ketika sekelompok parameter perlu dilewati pada suatu waktu, semacam mekanisme pengelompokan harus digunakan.
Dengan kata lain, ketika mengembangkan suatu fungsi, tanyakan pada diri Anda, "Bisakah saya memberikan parameter ini secara terpisah?" Jika jawabannya tidak, maka parameter harus dikelompokkan.
Mari kita lihat beberapa contoh:
// . // , let add xy = x + y // // , let locateOnMap (xCoord,yCoord) = // // // - type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = // let setCustomerName first last = // // // // , let setCustomerName myCredentials aName = //
Akhirnya, pastikan bahwa urutan parameter membantu aplikasi parsial (lihat manual di sini ). Misalnya, mengapa saya meletakkan myCredentials
depan nama di fungsi terakhir?
Fungsi tanpa parameter
Terkadang Anda mungkin memerlukan fungsi yang tidak menerima parameter apa pun. Misalnya, Anda memerlukan fungsi "hello world" yang dapat dipanggil beberapa kali. Seperti yang ditunjukkan pada bagian sebelumnya, definisi naif tidak berfungsi.
let sayHello = printfn "Hello World!" //
Tapi ini bisa diperbaiki dengan menambahkan parameter unit ke fungsi atau menggunakan lambda.
let sayHello() = printfn "Hello World!" // let sayHello = fun () -> printfn "Hello World!" //
Setelah itu, fungsi harus selalu dipanggil dengan argumen unit
:
// sayHello()
Apa yang terjadi cukup sering ketika berinteraksi dengan .NET libraries:
Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory()
Ingat, panggil mereka dengan parameter unit
!
Menentukan operator baru
Anda dapat menetapkan fungsi menggunakan satu atau beberapa karakter operator (lihat dokumentasi untuk daftar karakter):
// let (.*%) xy = x + y + 1
Anda harus menggunakan tanda kurung di sekitar karakter untuk menentukan fungsi.
Operator mulai dengan *
memerlukan ruang antara tanda kurung dan *
, karena di F # (*
bertindak sebagai awal komentar (seperti /*...*/
dalam C #):
let ( *+* ) xy = x + y + 1
Setelah ditentukan, fungsi baru dapat digunakan dengan cara biasa jika dibungkus dengan tanda kurung:
let result = (.*%) 2 3
Jika fungsi ini digunakan dengan dua parameter, Anda dapat menggunakan catatan operator infiks tanpa tanda kurung.
let result = 2 .*% 3
Anda juga dapat menentukan operator awalan dimulai dengan !
atau ~
(dengan beberapa batasan, lihat dokumentasi )
let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello"
Dalam F #, mendefinisikan pernyataan adalah operasi yang cukup umum, dan banyak perpustakaan akan mengekspor pernyataan dengan nama seperti >=>
dan <*>
.
Gaya bebas poin
Kami telah melihat banyak contoh fungsi yang tidak memiliki parameter terbaru untuk mengurangi tingkat kekacauan. Gaya ini disebut gaya point-free atau pemrograman diam - diam .
Berikut ini beberapa contohnya:
let add xy = x + y // let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 // let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list // let sum = List.reduce (+) // point free
Gaya ini memiliki pro dan kontra.
Salah satu kelebihannya adalah bahwa penekanannya adalah pada komposisi fungsi tingkat tinggi alih-alih sibuk dengan objek tingkat rendah. Misalnya, " (+) 1 >> (*) 2
" adalah tambahan eksplisit diikuti oleh perkalian. Dan " List.reduce (+)
" memperjelas bahwa operasi penambahan itu penting, terlepas dari informasi daftar.
Gaya pointless memungkinkan Anda untuk fokus pada algoritma dasar dan mengidentifikasi fitur-fitur umum dalam kode. Fungsi " reduce
" yang digunakan di atas adalah contoh yang bagus. Topik ini akan dibahas dalam seri yang direncanakan tentang pemrosesan daftar.
Di sisi lain, penggunaan berlebihan gaya seperti itu dapat membuat kode tidak jelas. Parameter eksplisit bertindak sebagai dokumentasi dan namanya (seperti "daftar") membuatnya lebih mudah untuk memahami apa fungsinya.
Seperti segala sesuatu dalam pemrograman, rekomendasi terbaik adalah memilih pendekatan yang memberikan kejelasan paling.
Combinators
" Combinators " disebut fungsi yang hasilnya hanya bergantung pada parameternya. Ini berarti bahwa tidak ada ketergantungan pada dunia luar, dan, khususnya, tidak ada fungsi lain atau nilai global yang dapat memengaruhi mereka.
Dalam praktiknya, ini berarti bahwa fungsi kombinatorial dibatasi oleh kombinasi parameternya dengan berbagai cara.
Kami telah melihat beberapa combinator: pipa dan operator komposisi. Jika Anda melihat definisi mereka, maka jelas bahwa semua yang mereka lakukan adalah menyusun ulang parameter dengan berbagai cara.
let (|>) xf = fx // pipe let (<|) fx = fx // pipe let (>>) fgx = g (fx) // let (<<) gfx = g (fx) //
Di sisi lain, fungsi-fungsi seperti "printf", meskipun primitif, bukan kombinator karena mereka bergantung pada dunia luar (I / O).
Burung kombinasi
Combinator adalah dasar dari seluruh bagian logika (secara alami disebut "logika kombinatorial"), yang diciptakan bertahun-tahun sebelum komputer dan bahasa pemrograman. Logika kombinatorial memiliki pengaruh yang sangat besar pada pemrograman fungsional.
Untuk mempelajari lebih lanjut tentang kombinator dan logika kombinatorial, saya merekomendasikan buku Raymond Smullyan "To Mock a Mockingbird." Di dalamnya, ia menjelaskan kombinator lain dan dengan fasih memberi mereka nama burung . Berikut adalah beberapa contoh combinator standar dan nama burung mereka:
let I x = x // , Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling // ... let rec Y fx = f (Y f) x // Y-, Sage bird
Nama hurufnya cukup standar, sehingga Anda bisa merujuk pada K-combinator kepada siapa saja yang akrab dengan terminologi ini.
Ternyata banyak pola pemrograman umum dapat direpresentasikan melalui kombinator standar ini. Misalnya, Kestrel adalah pola reguler di antarmuka yang lancar di mana Anda melakukan sesuatu tetapi mengembalikan objek asli. Thrush adalah pipa, Queer adalah komposisi langsung, dan Y-combinator melakukan pekerjaan yang sangat baik untuk menciptakan fungsi rekursif.
Bahkan, ada teorema terkenal bahwa fungsi yang dapat dihitung dapat dibangun hanya dengan menggunakan dua combinator dasar, Kestrel dan Starling.
Perpustakaan Kombinatorial
Pustaka kombinatorial adalah pustaka yang mengekspor banyak fungsi kombinatorial yang dirancang untuk dibagikan. Pengguna perpustakaan seperti itu dapat dengan mudah menggabungkan fungsi bersama untuk mendapatkan fungsi yang lebih besar dan lebih kompleks, seperti kubus dengan mudah.
Perpustakaan combiner yang dirancang dengan baik memungkinkan Anda untuk fokus pada fungsi tingkat tinggi, dan menyembunyikan "noise" tingkat rendah. Kami telah melihat kekuatan mereka dalam beberapa contoh dalam seri "mengapa menggunakan F #", dan modul List
penuh dengan fungsi-fungsi seperti itu, " fold
" dan " map
" juga merupakan kombinator jika Anda memikirkannya.
Keuntungan lain dari combinator adalah mereka adalah jenis fungsi yang paling aman. Karena mereka tidak memiliki ketergantungan pada dunia luar, mereka tidak dapat berubah ketika lingkungan global berubah. Fungsi yang membaca nilai global atau menggunakan fungsi pustaka dapat memecah atau mengubah antara panggilan jika konteksnya berubah. Ini tidak akan pernah terjadi pada kombinator.
Di F #, pustaka kombinator tersedia untuk parsing (FParsec), membuat HTML, kerangka kerja pengujian, dll. Kami akan membahas dan menggunakan kombinator nanti di seri berikutnya.
Fungsi rekursif
Seringkali suatu fungsi perlu merujuk dirinya sendiri dari tubuhnya. Contoh klasik adalah fungsi Fibonacci.
let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Sayangnya, fungsi ini tidak dapat dikompilasi:
error FS0039: The value or constructor 'fib' is not defined
Anda harus memberi tahu kompiler bahwa ini adalah fungsi rekursif menggunakan kata kunci rec
.
let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Fungsi rekursif dan struktur data sangat umum dalam pemrograman fungsional, dan saya berharap untuk mencurahkan seluruh seri untuk topik ini nanti.
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.