Mengapa ada dua macam fungsi dalam Elixir?

279

Saya belajar Elixir dan bertanya-tanya mengapa ia memiliki dua jenis definisi fungsi:

  • fungsi yang didefinisikan dalam modul dengan def, disebut usingmyfunction(param1, param2)
  • fungsi anonim didefinisikan dengan fn, disebut menggunakanmyfn.(param1, param2)

Hanya jenis fungsi kedua yang tampaknya merupakan objek kelas satu dan dapat diteruskan sebagai parameter ke fungsi lainnya. Fungsi yang didefinisikan dalam modul harus dibungkus dengan a fn. Ada beberapa gula sintaksis yang kelihatannya otherfunction(myfunction(&1, &2))untuk membuatnya mudah, tetapi mengapa itu perlu di tempat pertama? Kenapa kita tidak bisa begitu saja otherfunction(myfunction))? Apakah hanya mengizinkan fungsi memanggil modul tanpa tanda kurung seperti di Ruby? Tampaknya telah mewarisi karakteristik ini dari Erlang yang juga memiliki fungsi dan kesenangan modul, jadi apakah itu sebenarnya berasal dari cara kerja VM Erlang secara internal?

Adakah manfaat memiliki dua jenis fungsi dan mengkonversi dari satu jenis ke fungsi lain untuk meneruskannya ke fungsi lain? Apakah ada manfaat memiliki dua notasi berbeda untuk memanggil fungsi?

Alex Marandon
sumber

Jawaban:

386

Hanya untuk memperjelas penamaan, keduanya berfungsi. Satu adalah fungsi bernama dan yang lainnya adalah fungsi anonim. Tetapi Anda benar, mereka bekerja agak berbeda dan saya akan menggambarkan mengapa mereka bekerja seperti itu.

Mari kita mulai dengan yang kedua fn,. fnadalah penutupan, mirip dengan lambdadi Ruby. Kita bisa membuatnya sebagai berikut:

x = 1
fun = fn y -> x + y end
fun.(2) #=> 3

Suatu fungsi dapat memiliki beberapa klausa juga:

x = 1
fun = fn
  y when y < 0 -> x - y
  y -> x + y
end
fun.(2) #=> 3
fun.(-2) #=> 3

Sekarang, mari kita coba sesuatu yang berbeda. Mari kita coba mendefinisikan beberapa klausa yang berbeda yang mengharapkan sejumlah argumen:

fn
  x, y -> x + y
  x -> x
end
** (SyntaxError) cannot mix clauses with different arities in function definition

Oh tidak! Kami mendapat kesalahan! Kami tidak dapat mencampur klausa yang mengharapkan jumlah argumen yang berbeda. Suatu fungsi selalu memiliki arity tetap.

Sekarang, mari kita bicara tentang fungsi-fungsi yang disebutkan:

def hello(x, y) do
  x + y
end

Seperti yang diharapkan, mereka memiliki nama dan mereka juga dapat menerima beberapa argumen. Namun, mereka bukan penutupan:

x = 1
def hello(y) do
  x + y
end

Kode ini akan gagal dikompilasi karena setiap kali Anda melihat def, Anda mendapatkan lingkup variabel kosong. Itu adalah perbedaan penting di antara mereka. Saya terutama menyukai kenyataan bahwa setiap fungsi yang dinamai dimulai dengan yang bersih dan Anda tidak mendapatkan variabel dari cakupan yang berbeda semuanya tercampur menjadi satu. Anda memiliki batas yang jelas.

Kita dapat mengambil fungsi hello di atas sebagai fungsi anonim. Anda menyebutkannya sendiri:

other_function(&hello(&1))

Dan kemudian Anda bertanya, mengapa saya tidak bisa begitu saja lulus helloseperti dalam bahasa lain? Itu karena fungsi dalam Elixir diidentifikasi oleh nama dan arity. Jadi fungsi yang mengharapkan dua argumen adalah fungsi yang berbeda dari yang argumen tiga, bahkan jika mereka memiliki nama yang sama. Jadi jika kita lewat begitu saja hello, kita tidak akan tahu apa yang hellosebenarnya Anda maksudkan. Yang satu dengan dua, tiga atau empat argumen? Ini adalah alasan yang persis sama mengapa kita tidak dapat membuat fungsi anonim dengan klausa dengan arities yang berbeda.

Sejak Elixir v0.10.1, kami memiliki sintaks untuk menangkap fungsi bernama:

&hello/1

Itu akan menangkap fungsi bernama lokal halo dengan arity 1. Sepanjang bahasa dan dokumentasinya, sangat umum untuk mengidentifikasi fungsi dalam hello/1sintaks ini .

Ini juga mengapa Elixir menggunakan titik untuk memanggil fungsi anonim. Karena Anda tidak bisa begitu saja lulus hellosebagai fungsi, alih-alih Anda perlu menangkapnya secara eksplisit, ada perbedaan alami antara fungsi yang dinamai dan anonim dan sintaksis yang berbeda untuk memanggil masing-masing membuat semuanya sedikit lebih eksplisit (Lispers akan terbiasa dengan ini karena diskusi Lisp 1 vs. Lisp 2).

Secara keseluruhan, itulah alasan mengapa kita memiliki dua fungsi dan mengapa mereka berperilaku berbeda.

José Valim
sumber
30
Saya juga belajar Elixir, dan ini adalah masalah pertama yang saya temui yang memberi saya jeda, sesuatu tentang itu sepertinya tidak konsisten. Penjelasan yang bagus, tetapi untuk menjadi jelas ... apakah ini merupakan hasil dari masalah implementasi, atau apakah itu mencerminkan kebijaksanaan yang lebih dalam mengenai penggunaan dan kelulusan fungsi? Karena fungsi anonim dapat cocok berdasarkan nilai argumen, sepertinya akan berguna untuk dapat juga cocok dengan jumlah argumen (dan konsisten dengan pencocokan pola fungsi di tempat lain).
Topan
17
Ini bukan kendala implementasi dalam arti bahwa itu juga bisa berfungsi sebagai f()(tanpa titik).
José Valim
6
Anda dapat mencocokkan pada jumlah argumen dengan menggunakan is_function/2penjaga. is_function(f, 2)memeriksanya memiliki arity 2. :)
José Valim
20
Saya lebih suka pemanggilan fungsi tanpa titik untuk fungsi yang disebutkan dan anonim. Kadang-kadang membuatnya membingungkan dan Anda lupa apakah fungsi tertentu anonim atau bernama. Ada juga lebih banyak suara ketika datang ke kari.
CMCDragonkai
28
Di Erlang, panggilan fungsi anonim dan fungsi reguler sudah berbeda secara sintaksis: SomeFun()dan some_fun(). Dalam Elixir, jika kita menghapus titik, mereka akan sama some_fun()dan some_fun()karena variabel menggunakan pengidentifikasi yang sama dengan nama fungsi. Karena itu titik.
José Valim
17

Saya tidak tahu bagaimana ini akan berguna bagi orang lain, tetapi cara saya akhirnya membungkus kepala saya di sekitar konsep adalah untuk menyadari bahwa fungsi elixir bukan Fungsi.

Segala sesuatu dalam elixir adalah ekspresi. Begitu

MyModule.my_function(foo) 

bukan fungsi tetapi ekspresi yang dikembalikan dengan mengeksekusi kode di my_function. Sebenarnya hanya ada satu cara untuk mendapatkan "Function" yang bisa Anda berikan sebagai argumen dan itu adalah dengan menggunakan notasi fungsi anonim.

Sangat menggoda untuk merujuk ke fn atau & notasi sebagai penunjuk fungsi, tetapi sebenarnya jauh lebih. Ini adalah penutupan dari lingkungan sekitar.

Jika Anda bertanya pada diri sendiri:

Apakah saya memerlukan lingkungan eksekusi atau nilai data di tempat ini?

Dan jika Anda membutuhkan eksekusi menggunakan fn, maka sebagian besar kesulitan menjadi lebih jelas.

Fred Anjing Ajaib Ajaib
sumber
2
Jelas itu MyModule.my_function(foo)adalah ekspresi tetapi MyModule.my_function"bisa" adalah ekspresi yang mengembalikan fungsi "objek". Tetapi karena Anda perlu memberi tahu arity, Anda akan membutuhkan sesuatu seperti itu MyModule.my_function/1. Dan saya kira mereka memutuskan lebih baik menggunakan &MyModule.my_function(&1) sintaks yang memungkinkan untuk mengekspresikan arity (dan melayani tujuan lain juga). Masih belum jelas mengapa ada ()operator untuk fungsi yang disebutkan dan .()operator untuk fungsi yang tidak disebutkan namanya
RubenLaguna
Saya pikir Anda ingin: &MyModule.my_function/1itulah bagaimana Anda bisa menyebarkannya sebagai fungsi.
jnmandal
14

Saya tidak pernah mengerti mengapa penjelasan ini sangat rumit.

Ini benar-benar hanya perbedaan yang sangat kecil dikombinasikan dengan realitas "eksekusi fungsi tanpa parens" gaya Ruby.

Membandingkan:

def fun1(x, y) do
  x + y
end

Untuk:

fun2 = fn
  x, y -> x + y
end

Meskipun keduanya hanya pengidentifikasi ...

  • fun1adalah pengidentifikasi yang menjelaskan fungsi bernama yang didefinisikan dengan def.
  • fun2 adalah pengenal yang menggambarkan variabel (yang kebetulan mengandung referensi ke fungsi).

Pertimbangkan apa artinya itu ketika Anda melihat fun1atau fun2dalam ekspresi lain? Saat mengevaluasi ekspresi itu, apakah Anda memanggil fungsi yang direferensikan atau Anda hanya referensi nilai dari memori?

Tidak ada cara yang baik untuk mengetahui pada waktu kompilasi. Ruby memiliki kemewahan untuk mengintrospeksi ruang nama variabel untuk mengetahui apakah suatu variabel mengikat telah membayangi suatu fungsi pada suatu saat. Elixir, sedang dikompilasi, tidak bisa melakukan ini. Itulah yang dilakukan notasi titik, ia memberi tahu Elixir bahwa itu harus berisi referensi fungsi dan harus dipanggil.

Dan ini sangat sulit. Bayangkan bahwa tidak ada notasi titik. Pertimbangkan kode ini:

val = 5

if :rand.uniform < 0.5 do
  val = fn -> 5 end
end

IO.puts val     # Does this work?
IO.puts val.()  # Or maybe this?

Mengingat kode di atas, saya pikir cukup jelas mengapa Anda harus memberi petunjuk kepada Elixir. Bayangkan jika setiap variabel de-referensi harus memeriksa fungsi? Atau, bayangkan heroik apa yang diperlukan untuk selalu menyimpulkan bahwa dereferensi variabel menggunakan fungsi?

Jayson
sumber
12

Saya mungkin salah karena tidak ada yang menyebutkannya, tetapi saya juga mendapat kesan bahwa alasan untuk ini juga merupakan warisan rubi untuk dapat memanggil fungsi tanpa tanda kurung.

Arity jelas terlibat tetapi mari kita mengesampingkannya untuk sementara waktu dan menggunakan fungsi tanpa argumen. Dalam bahasa seperti javascript di mana tanda kurung adalah wajib, mudah untuk membuat perbedaan antara melewati fungsi sebagai argumen dan memanggil fungsi. Anda menyebutnya hanya saat Anda menggunakan tanda kurung.

my_function // argument
(function() {}) // argument

my_function() // function is called
(function() {})() // function is called

Seperti yang Anda lihat, memberi nama atau tidak tidak membuat perbedaan besar. Tapi elixir dan ruby ​​memungkinkan Anda untuk memanggil fungsi tanpa tanda kurung. Ini adalah pilihan desain yang saya pribadi suka tetapi memiliki efek samping ini Anda tidak dapat menggunakan nama saja tanpa tanda kurung karena itu bisa berarti Anda ingin memanggil fungsi. Ini untuk apa &. Jika Anda meninggalkan appity arity sesaat, menambahkan nama fungsi Anda dengan &berarti bahwa Anda secara eksplisit ingin menggunakan fungsi ini sebagai argumen, bukan apa fungsi ini mengembalikan.

Sekarang fungsi anonim sedikit berbeda karena sebagian besar digunakan sebagai argumen. Sekali lagi ini adalah pilihan desain tetapi rasional di balik itu adalah bahwa itu terutama digunakan oleh iterators jenis fungsi yang mengambil fungsi sebagai argumen. Jadi jelas Anda tidak perlu menggunakan &karena mereka sudah dianggap sebagai argumen secara default. Itu tujuan mereka.

Sekarang masalah terakhir adalah bahwa kadang-kadang Anda harus memanggil mereka dalam kode Anda, karena mereka tidak selalu digunakan dengan jenis fungsi iterator, atau Anda mungkin membuat kode iterator sendiri. Untuk cerita kecil, karena ruby ​​berorientasi objek, cara utama untuk melakukannya adalah menggunakan callmetode pada objek. Dengan begitu, Anda bisa menjaga perilaku kurung non-wajib konsisten.

my_lambda.call
my_lambda.call()
my_lambda_with_arguments.call :h2g2, 42
my_lambda_with_arguments.call(:h2g2, 42)

Sekarang seseorang datang dengan jalan pintas yang pada dasarnya terlihat seperti metode tanpa nama.

my_lambda.()
my_lambda_with_arguments.(:h2g2, 42)

Sekali lagi, ini adalah pilihan desain. Sekarang elixir tidak berorientasi objek dan karenanya panggilan tidak menggunakan bentuk pertama pasti. Saya tidak dapat berbicara untuk José tetapi sepertinya bentuk kedua digunakan di elixir karena masih terlihat seperti pemanggilan fungsi dengan karakter tambahan. Cukup dekat dengan panggilan fungsi.

Saya tidak memikirkan semua pro dan kontra, tetapi sepertinya dalam kedua bahasa Anda bisa lolos hanya dengan tanda kurung selama Anda membuat tanda kurung wajib untuk fungsi anonim. Sepertinya itu adalah:

Tanda kurung VS Notasi sedikit berbeda

Dalam kedua kasus Anda membuat pengecualian karena Anda membuat keduanya berperilaku berbeda. Karena ada perbedaan, Anda dapat membuatnya menjadi jelas dan menggunakan notasi yang berbeda. Kurung wajib akan terlihat alami dalam banyak kasus tetapi sangat membingungkan ketika hal-hal tidak berjalan sesuai rencana.

Ini dia Sekarang ini mungkin bukan penjelasan terbaik di dunia karena saya menyederhanakan sebagian besar detail. Juga sebagian besar adalah pilihan desain dan saya mencoba memberikan alasan bagi mereka tanpa menilai mereka. Saya suka elixir, saya suka ruby, saya suka panggilan fungsi tanpa tanda kurung, tetapi seperti Anda, saya menemukan konsekuensinya cukup sesekali sesekali.

Dan di elixir, hanya titik tambahan ini, sedangkan di ruby ​​Anda memiliki blok di atas ini. Blok luar biasa dan saya terkejut betapa banyak yang dapat Anda lakukan hanya dengan blok, tetapi mereka hanya bekerja ketika Anda hanya membutuhkan satu fungsi anonim yang merupakan argumen terakhir. Maka karena Anda harus bisa berurusan dengan skenario lain, inilah seluruh metode / lambda / proc / block confusion.

Lagi pula ... ini di luar jangkauan.

Mig R
sumber
9

Ada posting blog yang bagus tentang perilaku ini: tautan

Dua jenis fungsi

Jika modul berisi ini:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).

Anda tidak bisa memotong dan menempelkan ini ke shell dan mendapatkan hasil yang sama.

Itu karena ada bug di Erlang. Modul di Erlang adalah urutan FORMULIR . Shell Erlang mengevaluasi urutan EKSPRESI . Dalam BENTUK Erlang bukan EKSPRESI .

double(X) -> 2*X.            in an Erlang module is a FORM

Double = fun(X) -> 2*X end.  in the shell is an EXPRESSION

Keduanya tidak sama. Sedikit kekonyolan ini telah menjadi Erlang selamanya, tetapi kami tidak menyadarinya dan kami belajar untuk hidup dengannya.

Dot menelepon fn

iex> f = fn(x) -> 2 * x end
#Function<erl_eval.6.17052888>
iex> f.(10)
20

Di sekolah saya belajar memanggil fungsi dengan menulis f (10) bukan f (10) - ini adalah "benar-benar" fungsi dengan nama seperti Shell.f (10) (ini adalah fungsi yang didefinisikan dalam shell) Bagian shell adalah implisit sehingga seharusnya disebut f (10).

Jika Anda membiarkannya seperti ini, perkirakan untuk menghabiskan dua puluh tahun ke depan dalam hidup Anda menjelaskan alasannya.

sobolevn
sumber
Saya tidak yakin tentang kegunaan menjawab pertanyaan OP secara langsung, tetapi tautan yang disediakan (termasuk bagian komentar) sebenarnya adalah IMO yang bagus dibaca untuk audiens target pertanyaan, yaitu kita yang baru belajar & belajar Elixir + Erlang .
Tautan sudah mati sekarang :(
AnilRedshift
2

Elixir memiliki kawat gigi opsional untuk berbagai fungsi, termasuk fungsi dengan 0 arity. Mari kita lihat contoh mengapa itu membuat sintaks pemanggilan yang terpisah menjadi penting:

defmodule Insanity do
  def dive(), do: fn() -> 1 end
end

Insanity.dive
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive() 
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive.()
# 1

Insanity.dive().()
# 1

Tanpa membuat perbedaan antara 2 jenis fungsi, kita tidak bisa mengatakan apa Insanity.diveartinya: mendapatkan fungsi itu sendiri, memanggilnya, atau juga memanggil fungsi anonim yang dihasilkan.

Gram
sumber
1

fn ->sintaks untuk menggunakan fungsi anonim. Melakukan var. () Hanya memberitahu elixir bahwa saya ingin Anda mengambil var itu dengan func di dalamnya dan menjalankannya alih-alih merujuk ke var sebagai sesuatu yang hanya memegang fungsi itu.

Elixir memiliki pola umum ini di mana alih-alih memiliki logika di dalam fungsi untuk melihat bagaimana sesuatu harus dieksekusi, kita memadukan pola fungsi yang berbeda berdasarkan pada jenis input yang kita miliki. Saya berasumsi inilah sebabnya kami mengacu pada hal-hal oleh arity dalam function_name/1arti.

Agak aneh membiasakan diri melakukan definisi fungsi singkatan (func (& 1), dll), tetapi berguna ketika Anda mencoba untuk mem-pipe atau membuat kode Anda singkat.

Ian
sumber
0

Hanya jenis fungsi kedua yang tampaknya merupakan objek kelas satu dan dapat diteruskan sebagai parameter ke fungsi lainnya. Fungsi yang didefinisikan dalam modul harus dibungkus dengan fn. Ada beberapa gula sintaksis yang terlihat otherfunction(myfunction(&1, &2))untuk membuatnya mudah, tetapi mengapa itu perlu di tempat pertama? Kenapa kita tidak bisa begitu saja otherfunction(myfunction))?

Anda dapat melakukan otherfunction(&myfunction/2)

Karena elixir dapat menjalankan fungsi tanpa tanda kurung (seperti myfunction), menggunakannya otherfunction(myfunction))akan mencoba mengeksekusi myfunction/0.

Jadi, Anda perlu menggunakan operator tangkap dan menentukan fungsi, termasuk arity, karena Anda dapat memiliki fungsi yang berbeda dengan nama yang sama. Jadi &myfunction/2,.

ryanwinchester
sumber