Tại sao có hai loại chức năng trong Elixir?


279

Tôi đang học Elixir và tự hỏi tại sao nó có hai loại định nghĩa hàm:

  • các hàm được định nghĩa trong một mô-đun với def, được gọi là sử dụngmyfunction(param1, param2)
  • các hàm ẩn danh được định nghĩa bằng fn, được gọi là sử dụngmyfn.(param1, param2)

Chỉ có loại chức năng thứ hai dường như là một đối tượng hạng nhất và có thể được truyền dưới dạng tham số cho các chức năng khác. Một hàm được định nghĩa trong một mô-đun cần phải được bọc trong a fn. Có một số đường cú pháp trông giống như otherfunction(myfunction(&1, &2))để làm cho nó dễ dàng, nhưng tại sao nó lại cần thiết ở nơi đầu tiên? Tại sao chúng ta không thể làm gì otherfunction(myfunction))? Có phải chỉ cho phép gọi các chức năng mô-đun mà không có dấu ngoặc đơn như trong Ruby? Dường như nó được thừa hưởng đặc tính này từ Erlang, cũng có các chức năng và mô-đun thú vị, vậy nó có thực sự đến từ cách Erlang VM hoạt động bên trong không?

Có lợi ích gì khi có hai loại chức năng và chuyển đổi từ loại này sang loại khác để chuyển chúng sang các chức năng khác không? Có một lợi ích có hai ký hiệu khác nhau để gọi các chức năng?

Câu trả lời:


386

Chỉ cần làm rõ việc đặt tên, cả hai đều có chức năng. Một là một chức năng được đặt tên và cái còn lại là một chức năng ẩn danh. Nhưng bạn đã đúng, họ làm việc hơi khác và tôi sẽ minh họa tại sao họ làm việc như vậy.

Hãy bắt đầu với cái thứ hai , fn. fnlà một bao đóng, tương tự như lambdatrong Ruby. Chúng ta có thể tạo nó như sau:

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

Một hàm cũng có thể có nhiều mệnh đề:

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

Bây giờ, hãy thử một cái gì đó khác nhau. Hãy thử xác định các mệnh đề khác nhau để mong đợi một số lượng đối số khác nhau:

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

Ôi không! Chúng tôi nhận được một lỗi! Chúng ta không thể trộn các mệnh đề mong đợi một số lượng đối số khác nhau. Một chức năng luôn luôn có một arity cố định.

Bây giờ, hãy nói về các chức năng được đặt tên:

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

Như mong đợi, họ có một cái tên và họ cũng có thể nhận được một số đối số. Tuy nhiên, chúng không đóng cửa:

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

Mã này sẽ không biên dịch được vì mỗi lần bạn nhìn thấy def , bạn sẽ nhận được một phạm vi biến trống. Đó là một sự khác biệt quan trọng giữa chúng. Tôi đặc biệt thích thực tế là mỗi hàm được đặt tên bắt đầu bằng một bảng rõ ràng và bạn không nhận được các biến của các phạm vi khác nhau được trộn lẫn với nhau. Bạn có một ranh giới rõ ràng.

Chúng ta có thể lấy hàm hello được đặt tên ở trên dưới dạng hàm ẩn danh. Bạn đã đề cập đến nó:

other_function(&hello(&1))

Và sau đó bạn hỏi, tại sao tôi không thể đơn giản vượt qua nó hellonhư trong các ngôn ngữ khác? Đó là bởi vì các chức năng trong Elixir được xác định theo tên arity. Vì vậy, một hàm mong đợi hai đối số là một hàm khác với một hàm mong đợi ba, ngay cả khi chúng có cùng tên. Vì vậy, nếu chúng tôi đơn giản thông qua hello, chúng tôi sẽ không biết hellobạn thực sự có ý gì. Một với hai, ba hoặc bốn đối số? Đây chính xác là cùng một lý do tại sao chúng ta không thể tạo một hàm ẩn danh với các mệnh đề với các mức độ khác nhau.

Kể từ Elixir v0.10.1, chúng tôi có một cú pháp để nắm bắt các hàm được đặt tên:

&hello/1

Điều đó sẽ nắm bắt chức năng được đặt tên cục bộ xin chào với arity 1. Trong suốt ngôn ngữ và tài liệu của nó, việc xác định các hàm trong hello/1cú pháp này là rất phổ biến .

Đây cũng là lý do tại sao Elixir sử dụng dấu chấm để gọi các hàm ẩn danh. Vì bạn không thể đơn giản chuyển qua hellonhư một hàm, thay vào đó bạn cần nắm bắt một cách rõ ràng, có một sự khác biệt tự nhiên giữa các hàm được đặt tên và ẩn danh và một cú pháp riêng biệt để gọi mỗi hàm làm cho mọi thứ rõ ràng hơn một chút (Lispers sẽ quen với điều này do cuộc thảo luận Lisp 1 so với Lisp 2).

Nhìn chung, đó là những lý do tại sao chúng ta có hai chức năng và tại sao chúng hoạt động khác nhau.


30
Tôi cũng đang học Elixir, và đây là vấn đề đầu tiên tôi gặp phải khiến tôi phải tạm dừng, một cái gì đó về nó dường như không nhất quán. Giải thích tuyệt vời, nhưng để rõ ràng là đây là kết quả của một vấn đề thực hiện, hay nó phản ánh sự khôn ngoan sâu sắc hơn về việc sử dụng và thông qua các chức năng? Vì các hàm ẩn danh có thể khớp dựa trên các giá trị đối số, có vẻ như sẽ hữu ích khi có thể khớp với số lượng đối số (và phù hợp với mẫu hàm khớp với nơi khác).
Lốc xoáy

17
Nó không phải là một ràng buộc thực hiện theo nghĩa là nó cũng có thể hoạt động như f()(không có dấu chấm).
Jose Valim

6
Bạn có thể phù hợp với số lượng đối số bằng cách sử dụng is_function/2trong một bảo vệ. is_function(f, 2)kiểm tra xem nó có mức độ nào là 2. :)
José Valim

20
Tôi sẽ có các yêu cầu chức năng ưa thích mà không có dấu chấm cho cả chức năng được đặt tên và ẩn danh. Nó làm cho nó đôi khi khó hiểu và bạn quên mất một chức năng cụ thể là ẩn danh hoặc được đặt tên. Cũng có nhiều tiếng ồn hơn khi nói đến cà ri.
CMCDragonkai

28
Trong Erlang, các lệnh gọi hàm ẩn danh và các hàm thông thường đã khác nhau về mặt cú pháp: SomeFun()some_fun(). Trong Elixir, nếu chúng ta xóa dấu chấm, chúng sẽ giống nhau some_fun()some_fun()vì các biến sử dụng cùng một mã định danh làm tên hàm. Do đó dấu chấm.
Jose Valim

17

Tôi không biết điều này sẽ hữu ích như thế nào đối với bất kỳ ai khác, nhưng cách cuối cùng tôi cũng xoay quanh khái niệm này là nhận ra rằng các chức năng của thuốc tiên không phải là Chức năng.

Tất cả mọi thứ trong thuốc tiên là một biểu hiện. Vì thế

MyModule.my_function(foo) 

không phải là một hàm mà là biểu thức được trả về bằng cách thực thi mã trong my_function. Thực tế chỉ có một cách để có được một "Hàm" mà bạn có thể chuyển qua làm đối số và đó là sử dụng ký hiệu hàm ẩn danh.

Thật hấp dẫn khi đề cập đến fn hoặc & ký hiệu là một con trỏ hàm, nhưng nó thực sự còn hơn thế nữa. Đó là sự đóng cửa của môi trường xung quanh.

Nếu bạn tự hỏi:

Tôi có cần một môi trường thực thi hoặc giá trị dữ liệu tại chỗ này không?

Và nếu bạn cần thực thi sử dụng fn, thì hầu hết các khó khăn trở nên rõ ràng hơn nhiều.


2
Rõ ràng đó MyModule.my_function(foo)là một biểu thức nhưng MyModule.my_function"có thể" là một biểu thức trả về một "đối tượng" hàm. Nhưng vì bạn cần phải nói với arity, MyModule.my_function/1thay vào đó bạn sẽ cần một cái gì đó như thế. Và tôi đoán rằng họ đã quyết định tốt hơn là sử dụng một &MyModule.my_function(&1) cú pháp thay vì cho phép thể hiện sự tốt đẹp (và cũng phục vụ các mục đích khác). Vẫn chưa rõ lý do tại sao có một ()toán tử cho các hàm được đặt tên và một .()toán tử cho các hàm không được đặt tên
RubenLaguna

Tôi nghĩ bạn muốn: đó là &MyModule.my_function/1cách bạn có thể vượt qua nó như một chức năng.
jnmandal

14

Tôi chưa bao giờ hiểu tại sao giải thích về điều này rất phức tạp.

Nó thực sự chỉ là một sự khác biệt nhỏ đặc biệt kết hợp với thực tế của "thực thi chức năng mà không có parens" theo phong cách Ruby.

Đối chiếu:

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

Đến:

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

Trong khi cả hai chỉ là định danh ...

  • fun1là một định danh mô tả một chức năng được đặt tên được xác định với def.
  • fun2 là một định danh mô tả một biến (điều đó xảy ra để chứa tham chiếu đến hàm).

Xem xét điều đó có nghĩa là gì khi bạn nhìn thấy fun1hoặc fun2trong một số biểu hiện khác? Khi đánh giá biểu thức đó, bạn có gọi hàm được tham chiếu hay bạn chỉ tham chiếu một giá trị ngoài bộ nhớ?

Không có cách nào tốt để biết vào thời gian biên dịch. Ruby có khả năng hướng nội không gian tên biến để tìm hiểu xem một ràng buộc biến có làm mờ một hàm tại một thời điểm nào đó không. Elixir, đang được biên soạn, thực sự không thể làm điều này. Đó là những gì ký hiệu dấu chấm làm, nó nói với Elixir rằng nó nên chứa một tham chiếu hàm và nó nên được gọi.

Và điều này thực sự khó khăn. Hãy tưởng tượng rằng không có một ký hiệu chấm. Xem xét mã này:

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?

Với mã trên, tôi nghĩ khá rõ ràng lý do tại sao bạn phải đưa ra gợi ý cho Elixir. Hãy tưởng tượng nếu mỗi biến tham chiếu biến phải kiểm tra một hàm? Ngoài ra, hãy tưởng tượng những gì anh hùng sẽ là cần thiết để luôn luôn suy luận rằng sự thay đổi quy định sử dụng một chức năng?


12

Tôi có thể sai vì không ai đề cập đến nó, nhưng tôi cũng có ấn tượng rằng lý do cho điều này cũng là di sản ruby ​​của việc có thể gọi các chức năng mà không cần dấu ngoặc.

Arity rõ ràng có liên quan nhưng hãy để nó sang một bên và sử dụng các chức năng mà không cần tranh luận. Trong một ngôn ngữ như javascript, trong đó dấu ngoặc là bắt buộc, thật dễ dàng để tạo sự khác biệt giữa việc truyền một hàm làm đối số và gọi hàm. Bạn gọi nó chỉ khi bạn sử dụng dấu ngoặc.

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

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

Như bạn có thể thấy, việc đặt tên hay không không tạo ra sự khác biệt lớn. Nhưng thuốc tiên và ruby ​​cho phép bạn gọi các chức năng mà không cần dấu ngoặc. Đây là một lựa chọn thiết kế mà cá nhân tôi thích nhưng nó có tác dụng phụ này, bạn không thể chỉ sử dụng tên mà không có dấu ngoặc vì nó có thể có nghĩa là bạn muốn gọi hàm. Đây là những gì &dành cho. Nếu bạn rời khỏi arity appart trong một giây, hãy thêm vào tên hàm của bạn với &nghĩa là bạn rõ ràng muốn sử dụng hàm này làm đối số, chứ không phải hàm này trả về.

Bây giờ hàm ẩn danh hơi khác ở chỗ nó chủ yếu được sử dụng làm đối số. Một lần nữa, đây là một sự lựa chọn thiết kế, nhưng lý do đằng sau nó là nó chủ yếu được sử dụng bởi các loại hàm lặp có chức năng làm đối số. Vì vậy, rõ ràng bạn không cần sử dụng &vì chúng đã được coi là đối số theo mặc định. Đó là mục đích của họ.

Bây giờ vấn đề cuối cùng là đôi khi bạn phải gọi chúng trong mã của mình, bởi vì chúng không phải lúc nào cũng được sử dụng với một loại hàm lặp, hoặc bạn có thể tự mã hóa một trình vòng lặp. Đối với câu chuyện nhỏ, vì ruby ​​là hướng đối tượng, nên cách chính để làm điều đó là sử dụng callphương thức trên đối tượng. Bằng cách đó, bạn có thể giữ hành vi dấu ngoặc không bắt buộc nhất quán.

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

Bây giờ ai đó đã đưa ra một phím tắt mà về cơ bản trông giống như một phương thức không có tên.

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

Một lần nữa, đây là một sự lựa chọn thiết kế. Bây giờ elixir không hướng đối tượng và do đó gọi không sử dụng mẫu đầu tiên cho chắc chắn. Tôi không thể nói cho Jose nhưng có vẻ như hình thức thứ hai đã được sử dụng trong thuốc tiên vì nó vẫn giống như một lời gọi hàm với một ký tự phụ. Nó đủ gần với một cuộc gọi chức năng.

Tôi đã không nghĩ về tất cả các ưu và nhược điểm, nhưng có vẻ như trong cả hai ngôn ngữ bạn có thể thoát khỏi chỉ với dấu ngoặc miễn là bạn đặt dấu ngoặc bắt buộc cho các hàm ẩn danh. Có vẻ như nó là:

Dấu ngoặc bắt buộc VS Ký hiệu hơi khác nhau

Trong cả hai trường hợp, bạn tạo ra một ngoại lệ vì bạn làm cho cả hai cư xử khác nhau. Vì có một sự khác biệt, bạn cũng có thể làm cho nó rõ ràng và đi cho các ký hiệu khác nhau. Các dấu ngoặc bắt buộc sẽ trông tự nhiên trong hầu hết các trường hợp nhưng rất khó hiểu khi mọi thứ không đi theo kế hoạch.

Bạn đi đây Bây giờ đây có thể không phải là lời giải thích tốt nhất trên thế giới vì tôi đã đơn giản hóa hầu hết các chi tiết. Ngoài ra hầu hết đó là sự lựa chọn thiết kế và tôi đã cố gắng đưa ra lý do cho chúng mà không phán xét chúng. Tôi yêu thuốc tiên, tôi yêu ruby, tôi thích các cuộc gọi chức năng không có dấu ngoặc, nhưng cũng giống như bạn, tôi thấy hậu quả khá sai lầm trong một thời gian.

Và trong thuốc tiên, nó chỉ là dấu chấm bổ sung này, trong khi trong viên ruby ​​bạn có khối trên đầu này. Các khối rất tuyệt vời và tôi ngạc nhiên về việc bạn có thể làm được bao nhiêu chỉ với các khối, nhưng chúng chỉ hoạt động khi bạn chỉ cần một hàm ẩn danh là đối số cuối cùng. Sau đó, vì bạn sẽ có thể xử lý các tình huống khác, ở đây có toàn bộ phương thức / lambda / Proc / khối nhầm lẫn.

Dù sao ... đây là ngoài phạm vi.


9

Có một bài viết blog tuyệt vời về hành vi này: liên kết

Hai loại chức năng

Nếu một mô-đun có chứa điều này:

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

Bạn không thể cắt và dán nó vào vỏ và nhận được kết quả tương tự.

Đó là bởi vì có một lỗi trong Erlang. Các mô-đun trong Erlang là chuỗi FORMS . Shell Erlang đánh giá một chuỗi các EXPRESSION . Trong MẪU Erlang không phải là RPR RÀNG .

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

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

Hai là không giống nhau. Một chút silliness này đã là Erlang mãi mãi nhưng chúng tôi đã không nhận thấy nó và chúng tôi đã học cách sống với nó.

Chấm trong cuộc gọi fn

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

Ở trường, tôi đã học cách gọi các hàm bằng cách viết f (10) chứ không phải f. (10) - đây là một hàm thực sự có tên như Shell.f (10) (đó là một hàm được định nghĩa trong vỏ) Phần vỏ là ngầm nên nó chỉ được gọi là f (10).

Nếu bạn để nó như thế này, bạn sẽ dành hai mươi năm tiếp theo để giải thích lý do.


Tôi không chắc về tính hữu ích khi trả lời trực tiếp câu hỏi OP, nhưng liên kết được cung cấp (bao gồm cả phần bình luận) thực sự là một IMO đọc tuyệt vời cho đối tượng mục tiêu của câu hỏi, tức là những người trong chúng ta mới và học Elixir + Erlang .

Liên kết đã chết ngay bây giờ :(
AnilRedshift

2

Elixir có các dấu ngoặc tùy chọn cho các hàm, bao gồm các hàm có 0 arity. Hãy xem một ví dụ về lý do tại sao nó làm cho một cú pháp gọi riêng biệt trở nên quan trọng:

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

Không tạo ra sự khác biệt giữa 2 loại hàm, chúng ta không thể nói điều gì Insanity.divecó nghĩa là: tự lấy một hàm, gọi nó hoặc cũng gọi hàm ẩn danh kết quả.


1

fn ->cú pháp là để sử dụng các hàm ẩn danh. Làm var. () Chỉ là nói với elixir rằng tôi muốn bạn lấy var đó với một func trong đó và chạy nó thay vì tham chiếu var như một cái gì đó chỉ giữ chức năng đó.

Elixir có một mẫu chung này trong đó thay vì có logic bên trong hàm để xem cách thực hiện một cái gì đó, chúng ta mẫu khớp với các hàm khác nhau dựa trên loại đầu vào nào chúng ta có. Tôi cho rằng đây là lý do tại sao chúng ta đề cập đến mọi thứ bởi arity theo function_name/1nghĩa.

Thật kỳ lạ khi làm quen với việc thực hiện các định nghĩa hàm tốc ký (func (& 1), v.v.), nhưng tiện dụng khi bạn đang cố gắng dẫn hoặc giữ mã của bạn ngắn gọn.


0

Chỉ có loại chức năng thứ hai dường như là một đối tượng hạng nhất và có thể được truyền dưới dạng tham số cho các chức năng khác. Một hàm được định nghĩa trong một mô-đun cần phải được bọc trong một fn. Có một số đường cú pháp trông giống như otherfunction(myfunction(&1, &2))để làm cho nó dễ dàng, nhưng tại sao nó lại cần thiết ở nơi đầu tiên? Tại sao chúng ta không thể làm gì otherfunction(myfunction))?

Bạn có thể làm otherfunction(&myfunction/2)

Vì elixir có thể thực thi các chức năng mà không cần dấu ngoặc (như myfunction), sử dụng otherfunction(myfunction))nó sẽ cố gắng thực thi myfunction/0.

Vì vậy, bạn cần sử dụng toán tử chụp và chỉ định hàm, bao gồm cả arity, vì bạn có thể có các hàm khác nhau có cùng tên. Như vậy , &myfunction/2.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.