Sự khác biệt giữa phương thức và chức năng trong Scala


254

Tôi đọc Hàm Scala (một phần của Chuyến tham quan khác của Scala ). Trong bài đăng đó, ông tuyên bố:

Phương thức và chức năng không giống nhau

Nhưng anh không giải thích bất cứ điều gì về nó. Anh ấy đang cố nói gì vậy?



3
Tôi nghĩ rằng bạn có thể nhận được một cái gì đó từ sự khác biệt giữa một phương thức và một hàm
jinglining

Một câu hỏi tiếp theo với câu trả lời hay: Hàm và phương thức trong Scala
Josiah Yoder

Câu trả lời:


238

Jim đã nhận được khá nhiều điều này trong bài đăng trên blog của mình , nhưng tôi đang đăng một bản tóm tắt ở đây để tham khảo.

Trước tiên, hãy xem những gì Đặc tả Scala cho chúng ta biết. Chương 3 (loại) cho chúng tôi biết về Loại chức năng (3.2.9) và Loại phương thức (3.3.1). Chương 4 (khai báo cơ bản) nói về Tuyên bố giá trị và định nghĩa (4.1), Khai báo biến và định nghĩa (4.2) và Khai báo hàm và định nghĩa (4.6). Chương 6 (biểu thức) nói về Hàm ẩn danh (6.23) và Giá trị phương thức (6.7). Thật kỳ lạ, các giá trị hàm được nói đến một lần vào ngày 3.2.9, và không ở đâu khác.

Một Loại Chức năng là (xấp xỉ) một loại hình thức (T1, ..., Tn) => U , mà là một viết tắt cho các đặc điểm FunctionNtrong thư viện chuẩn. Các hàmgiá trị phương thức ẩn danh có các loại hàm và các loại hàm có thể được sử dụng như một phần của khai báo giá trị, biến và hàm và định nghĩa. Trong thực tế, nó có thể là một phần của kiểu phương thức.

Một phương pháp Loại là một loại không có giá trị . Điều đó có nghĩa là không có giá trị - không có đối tượng, không có thể hiện - với một kiểu phương thức. Như đã đề cập ở trên, Giá trị Phương thức thực sự có Loại Chức năng . Một kiểu phương thức là một defkhai báo - tất cả mọi thứ về một defngoại trừ cơ thể của nó.

Giá trị khai báo và định nghĩakhai báo biến và định nghĩavalvartờ khai, bao gồm cả hai loại và giá trị - có thể, tương ứng, Chức năng Loạichức năng Anonymous hoặc giá trị Phương pháp . Lưu ý rằng, trên JVM, các (giá trị phương thức) này được triển khai với cái mà Java gọi là "phương thức".

Một tuyên bố chức năng là một deftuyên bố, bao gồm cả loạicơ thể . Phần loại là Kiểu phương thức và phần thân là biểu thức hoặc khối . Điều này cũng được triển khai trên JVM với cái mà Java gọi là "phương thức".

Cuối cùng, Hàm ẩn danh là một thể hiện của Loại chức năng (nghĩa là một thể hiện của đặc điểm FunctionN) và Giá trị phương thức là điều tương tự! Điểm khác biệt là Giá trị phương thức được tạo từ các phương thức, bằng cách đặt trước một dấu gạch dưới ( m _là giá trị phương thức tương ứng với "khai báo hàm" ( def) m) hoặc bằng một quá trình gọi là mở rộng eta , giống như một quá trình tự động từ phương thức để hoạt động.

Đó là những gì thông số kỹ thuật nói, vì vậy hãy để tôi đưa ra điều này: chúng tôi không sử dụng thuật ngữ đó! Nó dẫn đến quá nhiều nhầm lẫn giữa cái gọi là "khai báo hàm" , là một phần của chương trình (chương 4 - khai báo cơ bản) và "hàm ẩn danh" , là một biểu thức và "kiểu hàm" , đó là, cũng là một loại - một đặc điểm.

Thuật ngữ dưới đây và được sử dụng bởi các lập trình viên Scala có kinh nghiệm, thực hiện một thay đổi so với thuật ngữ của đặc tả: thay vì nói khai báo hàm , chúng tôi nói phương thức . Hoặc thậm chí khai báo phương pháp. Hơn nữa, chúng tôi lưu ý rằng khai báo giá trịkhai báo biến cũng là phương pháp cho các mục đích thực tế.

Vì vậy, với sự thay đổi ở trên về thuật ngữ, đây là một lời giải thích thực tế về sự khác biệt.

Một chức năng là một đối tượng trong đó bao gồm một trong những FunctionXđặc điểm, chẳng hạn như Function0, Function1, Function2, vv Nó có thể bao gồm cả PartialFunctionlà tốt, mà thực sự mở rộng Function1.

Hãy xem chữ ký loại cho một trong những đặc điểm sau:

trait Function2[-T1, -T2, +R] extends AnyRef

Đặc điểm này có một phương thức trừu tượng (nó cũng có một vài phương thức cụ thể):

def apply(v1: T1, v2: T2): R

Và điều đó cho chúng ta biết tất cả những gì cần biết về nó. Một hàm có một applyphương thức nhận N tham số của các loại T1 , T2 , ..., TN và trả về một kiểu nào đó R. Nó là biến thể đối với các tham số mà nó nhận được và đồng biến thể trên kết quả.

Phương sai đó có nghĩa là a Function1[Seq[T], String]là một kiểu con của Function1[List[T], AnyRef]. Trở thành một kiểu con có nghĩa là nó có thể được sử dụng thay cho nó. Người ta có thể dễ dàng thấy rằng nếu tôi sẽ gọi f(List(1, 2, 3))và mong đợi sự AnyReftrở lại, một trong hai loại trên sẽ hoạt động.

Bây giờ, sự giống nhau của một phương thức và một hàm là gì? Chà, nếu flà một hàm và mlà một phương thức cục bộ trong phạm vi, thì cả hai có thể được gọi như thế này:

val o1 = f(List(1, 2, 3))
val o2 = m(List(1, 2, 3))

Các cuộc gọi này thực sự khác nhau, bởi vì cuộc gọi đầu tiên chỉ là một cú pháp cú pháp. Scala mở rộng nó thành:

val o1 = f.apply(List(1, 2, 3))

Mà, tất nhiên, là một phương thức gọi đối tượng f. Các hàm cũng có các loại cú pháp cú pháp khác với lợi thế của nó: hàm chữ (thực tế là hai trong số chúng) và (T1, T2) => Rchữ ký loại. Ví dụ:

val f = (l: List[Int]) => l mkString ""
val g: (AnyVal) => String = {
  case i: Int => "Int"
  case d: Double => "Double"
  case o => "Other"
}

Một điểm tương đồng khác giữa một phương thức và một hàm là cái trước có thể dễ dàng chuyển đổi thành cái sau:

val f = m _

Scala sẽ mở rộng điều đó , giả sử mloại là (List[Int])AnyRefvào (Scala 2.7):

val f = new AnyRef with Function1[List[Int], AnyRef] {
  def apply(x$1: List[Int]) = this.m(x$1)
}

Trên Scala 2.8, nó thực sự sử dụng một AbstractFunction1lớp để giảm kích thước lớp.

Lưu ý rằng người ta không thể chuyển đổi theo cách khác - từ một chức năng sang một phương thức.

Tuy nhiên, các phương thức có một lợi thế lớn (tốt, hai - chúng có thể nhanh hơn một chút): chúng có thể nhận được các tham số loại . Chẳng hạn, trong khi fở trên có thể nhất thiết chỉ định loại Listnhận được ( List[Int]trong ví dụ), mcó thể tham số hóa nó:

def m[T](l: List[T]): String = l mkString ""

Tôi nghĩ rằng điều này bao gồm khá nhiều thứ, nhưng tôi sẽ vui lòng bổ sung câu trả lời này cho bất kỳ câu hỏi nào còn tồn tại.


26
Lời giải thích này rất rõ ràng. Làm tốt. Thật không may, cả cuốn sách Oderky / Venners / Spoon và Scala đều sử dụng các từ "hàm" và "phương thức" có thể thay thế cho nhau. . .) Tôi nghĩ rằng việc sử dụng lỏng lẻo những từ này đã dẫn đến rất nhiều nhầm lẫn khi mọi người cố gắng học ngôn ngữ.
Seth Tisue

4
@ Tôi biết, tôi biết - PinS là cuốn sách dạy tôi Scala. Tôi đã học tốt hơn một cách khó khăn, tức là, paulp đặt tôi thẳng.
Daniel C. Sobral

4
Giải thích tuyệt vời! Tôi có một điều cần thêm: Khi bạn trích dẫn sự mở rộng của val f = mtrình biên dịch như val f = new AnyRef with Function1[List[Int], AnyRef] { def apply(x$1: List[Int]) = this.m(x$1) }bạn nên chỉ ra rằng thisbên trong applyphương thức không tham chiếu đến AnyRefđối tượng, mà là đối tượng trong đó phương thức val f = m _được đánh giá ( bên ngoài this , có thể nói là ), vì thislà một trong những giá trị được thu thập bởi bao đóng (như ví dụ returnnhư được chỉ ra bên dưới).
Holger Peine

1
@ DanielC.Sobral, cuốn sách PinS mà bạn đề cập là gì? Tôi cũng thích học Scala và không tìm thấy cuốn sách nào có tên đó,
tldr

5
@tldr Lập trình trong Scala , bởi Oderky et all. Đó là tên viết tắt phổ biến cho nó (họ đã nói với tôi rằng họ không thích PiS vì một số lý do! :)
Daniel C. Sobral

67

Một sự khác biệt thực tế lớn giữa một phương thức và một chức năng là những gì returncó nghĩa là. returnchỉ bao giờ trở lại từ một phương thức. Ví dụ:

scala> val f = () => { return "test" }
<console>:4: error: return outside method definition
       val f = () => { return "test" }
                       ^

Trở về từ một hàm được định nghĩa trong một phương thức thực hiện trả về không cục bộ:

scala> def f: String = {                 
     |    val g = () => { return "test" }
     | g()                               
     | "not this"
     | }
f: String

scala> f
res4: String = test

Trong khi đó trả về từ một phương thức cục bộ chỉ trả về từ phương thức đó.

scala> def f2: String = {         
     | def g(): String = { return "test" }
     | g()
     | "is this"
     | }
f2: String

scala> f2
res5: String = is this

9
Đó là bởi vì sự trở lại được nắm bắt bởi việc đóng cửa.
Daniel C. Sobral

4
Tôi không thể nghĩ về một lần duy nhất tôi muốn 'trở về' từ một chức năng sang phạm vi không nhắm mục tiêu. Trong thực tế, tôi có thể thấy rằng đó là một vấn đề bảo mật nghiêm trọng nếu một chức năng chỉ có thể quyết định nó muốn đi xa hơn vào ngăn xếp. Cảm thấy giống như longjmp, chỉ có cách dễ dàng hơn để vô tình nhận được sai. Mặc dù vậy, tôi đã nhận thấy scalac sẽ không cho phép tôi quay trở lại từ các chức năng. Điều đó có nghĩa là sự ghê tởm này đã bị tấn công từ ngôn ngữ?
root

2
@root - những gì về trở về từ bên trong a for (a <- List(1, 2, 3)) { return ... }? Điều đó được khử đường đến một đóng cửa.
Ben Lings

Hmm ... Vâng, đó là một trường hợp sử dụng hợp lý. Vẫn có khả năng dẫn đến các vấn đề khó gỡ lỗi khủng khiếp, nhưng điều đó đặt nó trong một bối cảnh hợp lý hơn.
root

1
Thành thật tôi sẽ sử dụng cú pháp khác nhau. đã returntrả về một giá trị từ hàm và một số dạng escapehoặc breakhoặc continuetrả về từ các phương thức.
Ryan The Leach

38

Hàm Một hàm có thể được gọi với một danh sách các đối số để tạo kết quả. Hàm có danh sách tham số, phần thân và loại kết quả. Các hàm là thành viên của một lớp, đặc điểm hoặc đối tượng singleton được gọi là các phương thức . Các hàm được định nghĩa bên trong các hàm khác được gọi là các hàm cục bộ. Các hàm với loại kết quả của Đơn vị được gọi là thủ tục. Các hàm ẩn danh trong mã nguồn được gọi là hàm chữ. Trong thời gian chạy, các ký tự hàm được khởi tạo thành các đối tượng được gọi là các giá trị hàm.

Lập trình trong Scala Phiên bản thứ hai. Martin Oderky - Chiếc thìa Lex - Bill Venners


1
Một hàm có thể thuộc về một lớp là def hoặc là val / var. Chỉ có def là phương thức.
Josiah Yoder

29

Giả sử bạn có một Danh sách

scala> val x =List.range(10,20)
x: List[Int] = List(10, 11, 12, 13, 14, 15, 16, 17, 18, 19)

Xác định một phương pháp

scala> def m1(i:Int)=i+2
m1: (i: Int)Int

Xác định hàm

scala> (i:Int)=>i+2
res0: Int => Int = <function1>

scala> x.map((x)=>x+2)
res2: List[Int] = List(12, 13, 14, 15, 16, 17, 18, 19, 20, 21)

Phương pháp chấp nhận đối số

scala> m1(2)
res3: Int = 4

Xác định hàm với val

scala> val p =(i:Int)=>i+2
p: Int => Int = <function1>

Đối số cho chức năng là Tùy chọn

 scala> p(2)
    res4: Int = 4

scala> p
res5: Int => Int = <function1>

Đối số về phương pháp là bắt buộc

scala> m1
<console>:9: error: missing arguments for method m1;
follow this method with `_' if you want to treat it as a partially applied function

Kiểm tra Hướng dẫn sau để giải thích việc chuyển các khác biệt với các ví dụ như ví dụ khác về hàm Phương thức Vs, Sử dụng hàm làm Biến, tạo hàm trả về hàm


13

Các chức năng không hỗ trợ mặc định tham số. Phương pháp làm. Chuyển đổi từ một phương thức sang một hàm làm mất các tham số mặc định. (Scala 2.8.1)


5
Có lý do cho điều này?
corazza

7

Có một bài viết hay ở đây mà hầu hết các mô tả của tôi được thực hiện. Chỉ là một so sánh ngắn về Chức năng và Phương pháp liên quan đến sự hiểu biết của tôi. Hy vọng nó giúp:

Chức năng : Chúng cơ bản là một đối tượng. Chính xác hơn, các hàm là các đối tượng với một phương thức áp dụng; Do đó, chúng chậm hơn một chút so với các phương thức vì chi phí hoạt động. Nó tương tự như các phương thức tĩnh theo nghĩa là chúng độc lập với một đối tượng được gọi. Một ví dụ đơn giản về hàm giống như dưới đây:

val f1 = (x: Int) => x + x
f1(2)  // 4

Dòng trên không có gì ngoài việc gán một đối tượng cho đối tượng khác như object1 = object2. Trên thực tế, object2 trong ví dụ của chúng ta là một hàm ẩn danh và phía bên trái có kiểu đối tượng vì điều đó. Do đó, bây giờ F1 là một đối tượng (Chức năng). Hàm ẩn danh thực sự là một thể hiện của Function1 [Int, Int] có nghĩa là một hàm có 1 tham số kiểu Int và giá trị trả về của kiểu Int. Gọi F1 mà không có đối số sẽ cho chúng ta chữ ký của hàm ẩn danh (Int => Int =)

Các phương thức : Chúng không phải là các đối tượng mà được gán cho một thể hiện của một lớp, tức là một đối tượng. Chính xác giống như phương thức trong java hoặc các hàm thành viên trong c ++ (như Raffi Khatchadourian đã chỉ ra trong một nhận xét cho câu hỏi này ) và v.v. Một ví dụ đơn giản về phương thức giống như dưới đây:

def m1(x: Int) = x + x
m1(2)  // 4

Dòng trên không phải là gán giá trị đơn giản mà là định nghĩa của phương thức. Khi bạn gọi phương thức này với giá trị 2 như dòng thứ hai, x được thay thế bằng 2 và kết quả sẽ được tính và bạn nhận được 4 làm đầu ra. Ở đây bạn sẽ gặp lỗi nếu chỉ đơn giản là viết m1 vì đây là phương thức và cần giá trị đầu vào. Bằng cách sử dụng _ bạn có thể gán một phương thức cho một hàm như dưới đây:

val f2 = m1 _  // Int => Int = <function1>

"Chỉ định một phương thức cho một hàm" nghĩa là gì? Có phải nó chỉ có nghĩa là bây giờ bạn có một đối tượng hành xử giống như cách mà phương thức đã làm không?
K. M

@KM: val f2 = m1 _ tương đương với val f2 = new Function1 [Int, Int] {def m1 (x: Int) = x + x};
sasuke

3

Đây là một bài viết tuyệt vời của Rob Norris giải thích sự khác biệt, đây là một TL; DR

Các phương thức trong Scala không phải là các giá trị, nhưng các hàm là. Bạn có thể xây dựng một hàm ủy nhiệm cho một phương thức thông qua mở rộng (được kích hoạt bởi dấu gạch dưới dấu gạch dưới).

với định nghĩa sau:

một phương thức là một cái gì đó được định nghĩa bằng def và một giá trị là thứ bạn có thể gán cho một val

Tóm lại ( trích từ blog ):

Khi chúng ta định nghĩa một phương thức, chúng ta thấy rằng chúng ta không thể gán nó cho a val.

scala> def add1(n: Int): Int = n + 1
add1: (n: Int)Int

scala> val f = add1
<console>:8: error: missing arguments for method add1;
follow this method with `_' if you want to treat it as a partially applied function
       val f = add1

Cũng lưu ý các loại của add1, mà không giống bình thường; bạn không thể khai báo một biến kiểu (n: Int)Int. Phương thức không phải là giá trị.

Tuy nhiên, bằng cách thêm toán tử postfix mở rộng (được phát âm là et eta), chúng ta có thể biến phương thức thành một giá trị hàm. Lưu ý các loại f.

scala> val f = add1 _
f: Int => Int = <function1>

scala> f(3)
res0: Int = 4

Tác dụng của _là thực hiện tương đương như sau: chúng tôi xây dựng một Function1thể hiện ủy nhiệm cho phương thức của chúng tôi.

scala> val g = new Function1[Int, Int] { def apply(n: Int): Int = add1(n) }
g: Int => Int = <function1>

scala> g(3)
res18: Int = 4

1

Trong Scala 2.13, không giống như các hàm, các phương thức có thể lấy / trả về

  • loại tham số (phương pháp đa hình)
  • tham số ngầm
  • loại phụ thuộc

Tuy nhiên, các hạn chế này được gỡ bỏ trong dotty (Scala 3) bởi các loại hàm đa hình # 4672 , ví dụ, phiên bản dotty 0.23.0-RC1 cho phép cú pháp sau

Loại tham số

def fmet[T](x: List[T]) = x.map(e => (e, e))
val ffun = [T] => (x: List[T]) => x.map(e => (e, e))

Tham số ngầm định ( tham số ngữ cảnh )

def gmet[T](implicit num: Numeric[T]): T = num.zero
val gfun: [T] => Numeric[T] ?=> T = [T] => (using num: Numeric[T]) => num.zero

Các loại phụ thuộc

class A { class B }
def hmet(a: A): a.B = new a.B
val hfun: (a: A) => a.B = hmet

Để biết thêm ví dụ, xem các bài kiểm tra / run / polymorphic-tests.scala


0

Thực tế, một lập trình viên Scala chỉ cần biết ba quy tắc sau để sử dụng đúng chức năng và phương thức:

  • Các phương thức được định nghĩa bởi defvà hàm chữ được định nghĩa bởi =>các hàm. Nó được định nghĩa trong trang 143, Chương 8 trong cuốn sách Lập trình ở Scala, ấn bản thứ 4.
  • Các giá trị hàm là các đối tượng có thể được truyền xung quanh dưới dạng bất kỳ giá trị nào. Hàm chữ và hàm được áp dụng một phần là giá trị hàm.
  • Bạn có thể bỏ dấu gạch dưới của hàm được áp dụng một phần nếu giá trị hàm được yêu cầu tại một điểm trong mã. Ví dụ:someNumber.foreach(println)

Sau bốn phiên bản Lập trình trong Scala, vấn đề là mọi người phải phân biệt hai khái niệm quan trọng: hàm và giá trị hàm vì tất cả các phiên bản không đưa ra lời giải thích rõ ràng. Đặc tả ngôn ngữ quá phức tạp. Tôi thấy các quy tắc trên là đơn giản và chính xác.

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.