Rõ ràng gọi trả lại trong một chức năng hay không


199

Cách đây không lâu, tôi đã bị Simon Urbanek khiển trách từ nhóm nòng cốt R (tôi tin) vì đã khuyến nghị người dùng gọi một cách rõ ràng returnkhi kết thúc một chức năng (mặc dù bình luận của anh ta đã bị xóa):

foo = function() {
  return(value)
}

thay vào đó ông đề nghị:

foo = function() {
  value
}

Có lẽ trong một tình huống như thế này, nó được yêu cầu:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Nhận xét của ông làm sáng tỏ lý do tại sao không gọi returntrừ khi thực sự cần thiết là một điều tốt, nhưng điều này đã bị xóa.

Câu hỏi của tôi là: Tại sao không gọi returnnhanh hơn hoặc tốt hơn, và do đó tốt hơn?


12
returnlà không cần thiết ngay cả trong ví dụ cuối cùng. Việc xóa returncó thể làm cho nó nhanh hơn một chút, nhưng theo quan điểm của tôi thì điều này là do R được cho là một ngôn ngữ lập trình thích hợp.
kohske

4
@kohske Bạn có thể mở rộng nhận xét của mình thành một câu trả lời, bao gồm nhiều chi tiết hơn về lý do tại sao nó nhanh hơn và làm thế nào điều này có liên quan đến R là một ngôn ngữ lập trình chức năng?
Paul Hiemstra

2
returngây ra bước nhảy không cục bộ và bước nhảy không cục bộ rõ ràng là không bình thường đối với FP. Trên thực tế, ví dụ, chương trình không có return. Tôi nghĩ rằng ý kiến ​​của tôi quá ngắn (và có thể không chính xác) như là một câu trả lời.
kohske

2
F # không có return, break, continuemột trong hai, đó là đôi khi tẻ nhạt.
colinfang

Câu trả lời:


129

Câu hỏi là: Tại sao không (rõ ràng) gọi trở lại nhanh hơn hoặc tốt hơn, và do đó thích hợp hơn?

Không có tuyên bố nào trong tài liệu R đưa ra giả định như vậy.
Trang chính? 'Hàm' nói:

function( arglist ) expr
return(value)

Có nhanh hơn mà không gọi trở lại?

Cả hai function()return()là các hàm nguyên thủy và function()chính nó trả về giá trị được đánh giá cuối cùng ngay cả khi không bao gồm return()hàm.

Gọi return()như .Primitive('return')với giá trị cuối cùng đó làm đối số sẽ thực hiện cùng một công việc nhưng cần thêm một cuộc gọi. Vì vậy, .Primitive('return')cuộc gọi không cần thiết này (thường) có thể rút thêm tài nguyên. Tuy nhiên, phép đo đơn giản cho thấy sự khác biệt kết quả là rất nhỏ và do đó không thể là lý do cho việc không sử dụng lợi nhuận rõ ràng. Biểu đồ sau được tạo từ dữ liệu được chọn theo cách này:

bench_nor2 <- function(x,repeats) { system.time(rep(
# without explicit return
(function(x) vector(length=x,mode="numeric"))(x)
,repeats)) }

bench_ret2 <- function(x,repeats) { system.time(rep(
# with explicit return
(function(x) return(vector(length=x,mode="numeric")))(x)
,repeats)) }

maxlen <- 1000
reps <- 10000
along <- seq(from=1,to=maxlen,by=5)
ret <- sapply(along,FUN=bench_ret2,repeats=reps)
nor <- sapply(along,FUN=bench_nor2,repeats=reps)
res <- data.frame(N=along,ELAPSED_RET=ret["elapsed",],ELAPSED_NOR=nor["elapsed",])

# res object is then visualized
# R version 2.15

Chức năng so sánh thời gian trôi qua

Hình ảnh trên có thể hơi khác nhau trên nền tảng của bạn. Dựa trên dữ liệu đo được, kích thước của đối tượng trả về không gây ra bất kỳ sự khác biệt nào, số lần lặp lại (ngay cả khi được tăng tỷ lệ) chỉ tạo ra một sự khác biệt rất nhỏ, mà trong từ thực với dữ liệu thực và thuật toán thực không thể được tính hoặc làm cho bạn kịch bản chạy nhanh hơn.

Có tốt hơn mà không gọi trở lại?

Return là công cụ tốt để thiết kế rõ ràng "lá" mã nơi thường trình nên kết thúc, nhảy ra khỏi hàm và trả về giá trị.

# here without calling .Primitive('return')
> (function() {10;20;30;40})()
[1] 40
# here with .Primitive('return')
> (function() {10;20;30;40;return(40)})()
[1] 40
# here return terminates flow
> (function() {10;20;return();30;40})()
NULL
> (function() {10;20;return(25);30;40})()
[1] 25
> 

Nó phụ thuộc vào chiến lược và phong cách lập trình của lập trình viên, anh ta sử dụng phong cách nào, anh ta không thể sử dụng return () vì nó không bắt buộc.

Lập trình viên lõi R sử dụng cả hai cách tiếp cận tức là. có và không có return return () vì có thể tìm thấy trong các nguồn của các hàm 'cơ sở'.

Nhiều lần chỉ return () được sử dụng (không có đối số) trả về NULL trong các trường hợp để dừng chức năng.

Không rõ liệu nó tốt hơn hay không như người dùng hoặc nhà phân tích tiêu chuẩn sử dụng R không thể thấy sự khác biệt thực sự.

Ý kiến ​​của tôi là câu hỏi nên là: Có bất kỳ nguy hiểm nào trong việc sử dụng lợi nhuận rõ ràng đến từ việc thực hiện R không?

Hoặc, có thể tốt hơn, người dùng viết mã chức năng nên luôn luôn hỏi: Hiệu quả của việc không sử dụng trả về rõ ràng (hoặc đặt đối tượng được trả về làm lá cuối cùng của nhánh mã) trong mã chức năng là gì?


4
Cảm ơn cho một câu trả lời rất tốt. Tôi tin rằng không có nguy hiểm trong việc sử dụng return, và nó tùy thuộc vào sở thích của người lập trình cho dù có sử dụng nó hay không.
Paul Hiemstra

38
Tốc độ returnthực sự là điều cuối cùng bạn nên lo lắng.
hadley

2
Tôi nghĩ rằng đây là một câu trả lời xấu. Những lý do dẫn đến sự bất đồng cơ bản về giá trị của việc sử dụng returncác lệnh gọi hàm không cần thiết . Câu hỏi bạn nên hỏi không phải là câu hỏi mà bạn đề xuất cuối cùng. Thay vào đó, đó là: Tại sao tôi nên sử dụng dự phòng return? Nó mang lại lợi ích gì? Hóa ra, câu trả lời là không có gì nhiều, hay thậm chí là không có gì khác. Trả lời của bạn không đánh giá cao điều này.
Konrad Rudolph

@KonradRudolph ... bạn đã lặp lại thực sự những gì Paul đã hỏi ban đầu (tại sao trả lại rõ ràng là xấu). Và tôi cũng muốn biết câu trả lời đúng (đúng cho bất kỳ ai) :). Bạn có xem xét để cung cấp lời giải thích của bạn cho người dùng của trang web này?
Petr Matousu

1
@Dason Tôi đã ở nơi khác liên kết một bài đăng Reddit giải thích lý do tại sao tranh luận này là thiếu sót trong bối cảnh này. Thật không may, bình luận dường như được tự động loại bỏ mỗi lần. Phiên bản ngắn returngiống như một nhận xét rõ ràng cho biết mức tăng x của 1 x 1, bên cạnh một đoạn mã đang làm x = x + 2. Nói cách khác, nhân chứng của nó là (a) hoàn toàn không liên quan, và (b) nó truyền đạt thông tin sai . Bởi vì returnngữ nghĩa của R là, hoàn toàn, đã hủy bỏ chức năng này. Nó không có nghĩa giống như returntrong các ngôn ngữ khác.
Konrad Rudolph

103

Nếu mọi người đồng ý rằng

  1. return không cần thiết ở cuối cơ thể
  2. không sử dụng returnnhanh hơn một chút (theo thử nghiệm của @ Alan, 4,3 micro giây so với 5.1)

tất cả chúng ta nên ngừng sử dụng returnở cuối một chức năng? Tôi chắc chắn sẽ không, và tôi muốn giải thích tại sao. Tôi hy vọng được nghe nếu người khác chia sẻ ý kiến ​​của tôi. Và tôi xin lỗi nếu đó không phải là một câu trả lời thẳng thắn cho OP, mà giống như một bình luận chủ quan dài.

Vấn đề chính của tôi khi không sử dụng returnlà, như Paul đã chỉ ra, có những nơi khác trong cơ thể của một chức năng mà bạn có thể cần nó. Và nếu bạn buộc phải sử dụng returnmột nơi nào đó ở giữa hàm của mình, tại sao không làm cho tất cả các returncâu lệnh rõ ràng? Tôi ghét không nhất quán. Ngoài ra tôi nghĩ rằng mã đọc tốt hơn; người ta có thể quét chức năng và dễ dàng nhìn thấy tất cả các điểm và giá trị thoát.

Paul đã sử dụng ví dụ này:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Thật không may, người ta có thể chỉ ra rằng nó có thể dễ dàng được viết lại thành:

foo = function() {
 if(a) {
   output <- a
 } else {
   output <- b
 }
output
}

Phiên bản sau thậm chí còn phù hợp với một số tiêu chuẩn mã hóa lập trình ủng hộ một câu lệnh return cho mỗi hàm. Tôi nghĩ một ví dụ tốt hơn có thể là:

bar <- function() {
   while (a) {
      do_stuff
      for (b) {
         do_stuff
         if (c) return(1)
         for (d) {
            do_stuff
            if (e) return(2)
         }
      }
   }
   return(3)
}

Điều này sẽ khó hơn nhiều để viết lại bằng cách sử dụng một câu lệnh return: nó sẽ cần nhiều breaks và một hệ thống phức tạp của các biến boolean để truyền bá chúng. Tất cả điều này để nói rằng quy tắc hoàn trả duy nhất không chơi tốt với R. Vì vậy, nếu bạn sẽ cần sử dụng returnở một số nơi trên cơ thể chức năng của mình, tại sao không nhất quán và sử dụng nó ở mọi nơi?

Tôi không nghĩ rằng đối số tốc độ là một đối số hợp lệ. Chênh lệch 0,8 micro giây không là gì khi bạn bắt đầu nhìn vào các chức năng thực sự làm gì đó. Điều cuối cùng tôi có thể thấy là nó ít gõ hơn nhưng này, tôi không lười biếng.


7
+1, có một nhu cầu rõ ràng cho returntuyên bố trong một số trường hợp, như @flodel cho thấy. Ngoài ra, có những tình huống trong đó một câu lệnh return được bỏ qua tốt nhất, ví dụ như rất nhiều lệnh gọi hàm nhỏ. Trong tất cả các trường hợp khác, giả sử 95%, các trường hợp không thực sự quan trọng nếu một người sử dụng returnhay không, và nó được đưa ra theo sở thích. Tôi thích sử dụng return vì nó rõ ràng hơn trong ý của bạn, do đó dễ đọc hơn. Có lẽ cuộc thảo luận này gần giống với <-vs =?
Paul Hiemstra

7
Điều này coi R là ngôn ngữ lập trình bắt buộc, không phải là ngôn ngữ lập trình chức năng. Lập trình hàm đơn giản là hoạt động khác nhau, và sử dụng returnđể trả về một giá trị là vô nghĩa, ngang bằng với văn bản if (x == TRUE)thay vì if (x).
Konrad Rudolph

4
Bạn cũng viết lại foodưới dạng foo <- function(x) if (a) a else b(với ngắt dòng khi cần thiết). Không cần trả lại rõ ràng hoặc giá trị trung gian.
hadley

26

Đây là một cuộc thảo luận thú vị. Tôi nghĩ rằng ví dụ của @ flodel là tuyệt vời. Tuy nhiên, tôi nghĩ rằng nó minh họa quan điểm của tôi (và @koshke đề cập đến điều này trong một bình luận) có returný nghĩa khi bạn sử dụng mệnh lệnh thay vì kiểu mã hóa chức năng .

Không tin vào quan điểm, nhưng tôi sẽ viết lại foonhư thế này:

foo = function() ifelse(a,a,b)

Một kiểu chức năng tránh các thay đổi trạng thái, như lưu trữ giá trị của output. Trong phong cách này, returnlà ra khỏi vị trí; footrông giống như một hàm toán học.

Tôi đồng ý với @flodel: sử dụng một hệ thống phức tạp của các biến boolean trong barsẽ ít rõ ràng hơn và vô nghĩa khi bạn có return. Điều làm cho barrất dễ để returntuyên bố là nó được viết theo phong cách bắt buộc. Thật vậy, các biến boolean đại diện cho các thay đổi "trạng thái" được tránh trong một kiểu chức năng.

Thật sự rất khó để viết lại bartheo phong cách chức năng, bởi vì nó chỉ là mã giả, nhưng ý tưởng là như thế này:

e_func <- function() do_stuff
d_func <- function() ifelse(any(sapply(seq(d),e_func)),2,3)
b_func <- function() {
  do_stuff
  ifelse(c,1,sapply(seq(b),d_func))
}

bar <- function () {
   do_stuff
   sapply(seq(a),b_func) # Not exactly correct, but illustrates the idea.
}

Các whilevòng lặp sẽ là khó khăn nhất để viết lại, bởi vì nó được điều khiển bởi những thay đổi trạng thái để a.

Mất tốc độ gây ra bởi một cuộc gọi đến returnlà không đáng kể, nhưng hiệu quả đạt được bằng cách tránh returnvà viết lại theo kiểu chức năng thường là rất lớn. Nói người dùng mới ngừng sử dụng returncó thể sẽ không giúp ích gì, nhưng hướng dẫn họ theo phong cách chức năng sẽ được đền đáp.


@Paul returnlà cần thiết theo kiểu bắt buộc vì bạn thường muốn thoát hàm tại các điểm khác nhau trong một vòng lặp. Một kiểu chức năng không sử dụng các vòng lặp, và do đó không cần return. Trong một phong cách chức năng thuần túy, cuộc gọi cuối cùng hầu như luôn luôn là giá trị trả về mong muốn.

Trong Python, các hàm yêu cầu một returncâu lệnh. Tuy nhiên, nếu bạn lập trình chức năng của mình theo kiểu chức năng, bạn có thể sẽ chỉ có mộtreturn câu lệnh: ở cuối chức năng của bạn.

Sử dụng một ví dụ từ một bài đăng StackOverflow khác, giả sử chúng tôi muốn một hàm trả về TRUEnếu tất cả các giá trị trong một giá trị xcó độ dài lẻ. Chúng tôi có thể sử dụng hai phong cách:

# Procedural / Imperative
allOdd = function(x) {
  for (i in x) if (length(i) %% 2 == 0) return (FALSE)
  return (TRUE)
}

# Functional
allOdd = function(x) 
  all(length(x) %% 2 == 1)

Trong một kiểu chức năng, giá trị được trả về tự nhiên rơi vào cuối của hàm. Một lần nữa, nó trông giống như một hàm toán học.

@GSee Các cảnh báo được nêu trong ?ifelsechắc chắn rất thú vị, nhưng tôi không nghĩ họ đang cố gắng từ chối sử dụng chức năng này. Trong thực tế, ifelsecó lợi thế của các chức năng vector hóa tự động. Ví dụ: hãy xem xét một phiên bản sửa đổi một chút của foo:

foo = function(a) { # Note that it now has an argument
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Hàm này hoạt động tốt khi length(a)là 1. Nhưng nếu bạn viết lại foobằng mộtifelse

foo = function (a) ifelse(a,a,b)

Bây giờ foohoạt động trên bất kỳ chiều dài của a. Trong thực tế, nó thậm chí sẽ hoạt động khi alà một ma trận. Trả lại một giá trị có hình dạng giống như testmột tính năng giúp vector hóa, không phải là một vấn đề.


Tôi không rõ tại sao returnkhông phù hợp với phong cách lập trình chức năng. Wether one đang lập trình bắt buộc hoặc theo chức năng, ở một giai đoạn nào đó, một hàm hoặc chương trình con cần trả về một cái gì đó. Ví dụ, lập trình chức năng trong python vẫn yêu cầu một returntuyên bố. Bạn có thể giải thích thêm về điểm này.
Paul Hiemstra

Trong tình huống này, sử dụng ifelse(a,a,b)là một tiểu thú cưng của tôi. Có vẻ như mọi dòng trong ?ifelseđều hét lên, "đừng sử dụng tôi thay vì if (a) {a} else b." ví dụ: "... trả về một giá trị có cùng hình dạng như test", "nếu yeshoặc noquá ngắn, các phần tử của chúng được tái chế.", "chế độ của kết quả có thể phụ thuộc vào giá trị của test", "thuộc tính lớp của kết quả được lấy từ testvà có thể không phù hợp với các giá trị được chọn từ yesno"
GSee

Ở cái nhìn thứ hai, fookhông có nhiều ý nghĩa; nó sẽ luôn trả về ĐÚNG hoặc b. Sử dụng ifelsenó sẽ trả về 1 hoặc một vài TRUE và / hoặc 1 hoặc vài bs. Ban đầu, tôi nghĩ mục đích của hàm là nói "nếu một câu lệnh nào đó là TRUE, hãy trả lại một cái gì đó, nếu không, trả lại một cái gì đó khác." Tôi không nghĩ rằng nên được vector hóa, bởi vì sau đó nó sẽ trở thành "trả lại các phần tử của một số đối tượng là TRUE và cho tất cả các phần tử không TRUE, quay lại b.
GSee

22

Dường như không có return()nó nhanh hơn ...

library(rbenchmark)
x <- 1
foo <- function(value) {
  return(value)
}
fuu <- function(value) {
  value
}
benchmark(foo(x),fuu(x),replications=1e7)
    test replications elapsed relative user.self sys.self user.child sys.child
1 foo(x)     10000000   51.36 1.185322     51.11     0.11          0         0
2 fuu(x)     10000000   43.33 1.000000     42.97     0.05          0         0

____ CHỈNH SỬA__ _ __ _ __ _ __ _ __ _ ___

Tôi tiến hành điểm chuẩn của người khác ( benchmark(fuu(x),foo(x),replications=1e7)) và kết quả bị đảo ngược ... Tôi sẽ thử trên máy chủ.


Bạn có thể bình luận về lý do tại sao sự khác biệt này xảy ra?
Paul Hiemstra

4
Câu trả lời của @PaulHiemstra Petr bao gồm một trong những lý do chính cho việc này; Hai cuộc gọi khi sử dụng return(), một cuộc gọi nếu bạn không. Nó hoàn toàn dư thừa ở cuối hàm khi function()trả về giá trị cuối cùng của nó. Bạn chỉ nhận thấy điều này trong nhiều lần lặp lại của một hàm trong đó không được thực hiện nhiều trong nội bộ để chi phí return()trở thành một phần lớn trong tổng thời gian tính toán của hàm.
Gavin Simpson

13

Một vấn đề với việc không đặt 'return' một cách rõ ràng vào cuối là nếu một người thêm các câu lệnh bổ sung vào cuối phương thức, đột nhiên giá trị trả về bị sai:

foo <- function() {
    dosomething()
}

Điều này trả về giá trị của dosomething().

Bây giờ chúng tôi đến cùng vào ngày hôm sau và thêm một dòng mới:

foo <- function() {
    dosomething()
    dosomething2()
}

Chúng tôi muốn mã của chúng tôi trả về giá trị của dosomething(), nhưng thay vào đó, nó không còn nữa.

Với một sự trở lại rõ ràng, điều này trở nên thực sự rõ ràng:

foo <- function() {
    return( dosomething() )
    dosomething2()
}

Chúng ta có thể thấy rằng có một cái gì đó kỳ lạ về mã này và sửa nó:

foo <- function() {
    dosomething2()
    return( dosomething() )
}

1
vâng, thực sự tôi thấy rằng return return () hữu ích khi gỡ lỗi; một khi mã được dọn sạch, nhu cầu của nó sẽ kém hấp dẫn hơn và tôi thích sự thanh lịch của việc không có nó ...
PatrickT

Nhưng đây không thực sự là một vấn đề trong mã thực, nó hoàn toàn là lý thuyết. Mã có thể chịu đựng điều này có một vấn đề lớn hơn nhiều: một dòng mã khó hiểu, dễ vỡ đến mức không rõ ràng đến mức các bổ sung đơn giản phá vỡ nó.
Konrad Rudolph

@KonradRudolph Tôi nghĩ rằng bạn đang làm một Scotsman không thật trên đó ;-) "Nếu đây là một vấn đề trong mã của bạn, bạn là một lập trình viên tồi!". Tôi không thực sự đồng ý. Tôi nghĩ rằng trong khi bạn có thể thoát khỏi việc rút ngắn các đoạn mã nhỏ, nơi bạn biết từng dòng chữ, điều này sẽ quay trở lại để cắn bạn khi mã của bạn lớn hơn.
Hugh Perkins

2
@HughPerkins Đó không phải là người Scotland thực thụ ; đúng hơn, đó là một quan sát thực nghiệm về độ phức tạp của mã, được hỗ trợ bởi hàng thập kỷ thực hành tốt nhất về công nghệ phần mềm: giữ cho các chức năng riêng lẻ ngắn và dòng mã rõ ràng. Và bỏ qua returnkhông phải là một phím tắt, đó là phong cách phù hợp trong lập trình chức năng. Sử dụng returncác cuộc gọi chức năng không cần thiết là một ví dụ của lập trình sùng bái hàng hóa .
Konrad Rudolph

Chà ... tôi không thấy làm thế nào điều này ngăn bạn thêm điều gì đó sau returntuyên bố của bạn và không nhận thấy rằng nó sẽ không được thực thi. Bạn chỉ cần cũng có thể thêm một bình luận sau khi giá trị bạn muốn quay trở lại ví dụdosomething() # this is my return value, don't add anything after it unless you know goddam well what you are doing
lebatsnok

10

Câu hỏi của tôi là: Tại sao không gọi returnnhanh hơn

Nó nhanh hơn bởi vì returnlà một hàm (nguyên thủy) trong R, có nghĩa là việc sử dụng nó trong mã sẽ phát sinh chi phí của một cuộc gọi hàm. So sánh điều này với hầu hết các ngôn ngữ lập trình khác, nơi returnlà một từ khóa, nhưng không phải là một hàm gọi: nó không dịch sang bất kỳ thực thi mã thời gian chạy nào.

Điều đó nói rằng, gọi một hàm nguyên thủy theo cách này là khá nhanh trong R và gọi là returnphát sinh chi phí rất nhỏ. Đây không phải là đối số để bỏ qua return.

hoặc tốt hơn, và do đó thích hợp hơn?

Bởi vì không có lý do để sử dụng nó.

Bởi vì nó dư thừa và nó không hữu ích sự dư thừa .

Để được rõ ràng: sự dư thừa có thể đôi khi có ích . Nhưng hầu hết sự dư thừa không phải là loại này. Thay vào đó, nó thuộc loại bổ sung sự lộn xộn thị giác mà không cần thêm thông tin: đó là chương trình tương đương với một từ điền hoặc chartjunk ).

Hãy xem xét ví dụ sau đây về một nhận xét giải thích, được công nhận trên toàn cầu là sự dư thừa xấu bởi vì nhận xét chỉ đơn thuần là diễn giải những gì mã đã thể hiện:

# Add one to the result
result = x + 1

Sử dụng returntrong R thuộc cùng loại, vì R là ngôn ngữ lập trình chức năng và trong R, mỗi lệnh gọi hàm đều có giá trị . Đây là một nền tảng thuộc tính của R. Và một khi bạn thấy mã R từ phối cảnh mà mọi biểu thức (bao gồm mọi lệnh gọi hàm) đều có một giá trị, thì câu hỏi sẽ trở thành: Tại sao tôi nên sử dụng return? Cần phải có một lý do tích cực, vì mặc định là không sử dụng nó.

Một lý do tích cực như vậy là để báo hiệu thoát sớm khỏi một chức năng, nói trong một mệnh đề bảo vệ :

f = function (a, b) {
    if (! precondition(a)) return() # same as `return(NULL)`!
    calculation(b)
}

Đây là một sử dụng hợp lệ, không dư thừa return. Tuy nhiên, các mệnh đề bảo vệ như vậy rất hiếm trong R so với các ngôn ngữ khác và vì mọi biểu thức đều có một giá trị, nên thông thường ifkhông yêu cầu return:

sign = function (num) {
    if (num > 0) {
        1
    } else if (num < 0) {
        -1
    } else {
        0
    }
}

Chúng tôi thậm chí có thể viết lại fnhư thế này:

f = function (a, b) {
    if (precondition(a)) calculation(b)
}

Sầu đâu if (cond) exprcũng giống như if (cond) expr else NULL.

Cuối cùng, tôi muốn kiểm tra ba phản đối phổ biến:

  1. Một số người lập luận rằng việc sử dụng returnthêm sự rõ ràng, bởi vì nó báo hiệu cho chức năng này, hàm này trả về giá trị. Nhưng như đã giải thích ở trên, mọi hàm đều trả về một cái gì đó trong R. Nghĩ về returnviệc đánh dấu một giá trị không chỉ là dư thừa, nó chủ động gây hiểu lầm .

  2. Liên quan, Zen của Python có một hướng dẫn tuyệt vời cần phải tuân theo:

    Rõ ràng là tốt hơn so với ngầm.

    Làm thế nào để giảm dư thừa returnkhông vi phạm điều này? Bởi vì giá trị trả về của hàm trong ngôn ngữ hàm luôn rõ ràng: đó là biểu thức cuối cùng của nó. Đây lại là một lập luận tương tự về nhân chứng vs. redundancy.

    Trong thực tế, nếu bạn muốn làm chứng, hãy sử dụng nó để làm nổi bật ngoại lệ cho quy tắc: đánh dấu các hàm không trả về giá trị có ý nghĩa, chỉ được gọi cho các tác dụng phụ của chúng (chẳng hạn như cat). Ngoại trừ R có điểm đánh dấu tốt hơn returntrong trường hợp này : invisible. Chẳng hạn, tôi sẽ viết

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
  3. Nhưng những gì về chức năng dài? Sẽ không dễ dàng để mất theo dõi những gì đang được trả lại?

    Hai câu trả lời: thứ nhất, không thực sự. Quy tắc rất rõ ràng: biểu thức cuối cùng của hàm là giá trị của nó. Không có gì để theo dõi.

    Nhưng quan trọng hơn, vấn đề trong các chức năng dài không phải là thiếu các returndấu hiệu rõ ràng . Đó là chiều dài của hàm . Các chức năng dài hầu như (?) Luôn vi phạm nguyên tắc trách nhiệm duy nhất và ngay cả khi chúng không hoạt động, chúng sẽ được hưởng lợi từ việc phân tách để dễ đọc.


Có lẽ tôi nên thêm rằng một số người ủng hộ việc sử dụng returnđể làm cho nó giống với các ngôn ngữ khác. Nhưng đó là một lập luận xấu: các ngôn ngữ lập trình chức năng khác có xu hướng không sử dụng return. Đó chỉ là ngôn ngữ bắt buộc , trong đó không phải mọi biểu thức đều có giá trị, sử dụng nó.
Konrad Rudolph

Tôi đã đến với Câu hỏi này với ý kiến ​​rằng việc sử dụng returnhỗ trợ rõ ràng tốt hơn và đọc câu trả lời của bạn với sự chỉ trích đầy đủ. Câu trả lời của bạn đã khiến tôi suy nghĩ về quan điểm đó. Tôi nghĩ rằng nhu cầu sử dụng returncho rõ ràng (ít nhất là trong trường hợp của riêng tôi), gắn liền với nhu cầu có thể sửa đổi các chức năng của tôi tốt hơn vào thời điểm sau. Với quan niệm rằng các chức năng của tôi đơn giản là quá phức tạp, giờ đây tôi có thể thấy rằng một mục tiêu để cải thiện phong cách lập trình của tôi, sẽ là cố gắng cấu trúc các mã để duy trì sự khám phá mà không cần a return. Cảm ơn bạn cho những phản ánh và cái nhìn sâu sắc !!
Kasper Thystrup Karstensen

6

Tôi nghĩ về returnmột mẹo. Theo nguyên tắc chung, giá trị của biểu thức cuối cùng được đánh giá trong hàm trở thành giá trị của hàm - và mẫu chung này được tìm thấy ở nhiều nơi. Tất cả các đánh giá sau đây đến 3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

(function() {
1
2
3
})()

Những gì returnkhông thực sự trả về một giá trị (điều này được thực hiện có hoặc không có nó) nhưng "phá vỡ" chức năng một cách bất thường. Theo nghĩa đó, nó là tương đương gần nhất với câu lệnh GOTO trong R (cũng có ngắt và tiếp theo). Tôi sử dụng returnrất hiếm khi và không bao giờ ở cuối một chức năng.

 if(a) {
   return(a)
 } else {
   return(b)
 }

... Điều này có thể được viết lại vì if(a) a else bnó dễ đọc hơn và ít bị uốn cong hơn. Không cần returnở tất cả ở đây. Trường hợp nguyên mẫu của tôi về việc sử dụng "trở lại" sẽ giống như ...

ugly <- function(species, x, y){
   if(length(species)>1) stop("First argument is too long.")
   if(species=="Mickey Mouse") return("You're kidding!")
   ### do some calculations 
   if(grepl("mouse", species)) {
      ## do some more calculations
      if(species=="Dormouse") return(paste0("You're sleeping until", x+y))
      ## do some more calculations
      return(paste0("You're a mouse and will be eating for ", x^y, " more minutes."))
      }
   ## some more ugly conditions
   # ...
   ### finally
   return("The end")
   }

Nói chung, nhu cầu trả lại nhiều gợi ý rằng vấn đề là xấu hoặc có cấu trúc xấu.

<>

return không thực sự cần một chức năng để làm việc: bạn có thể sử dụng nó để thoát ra khỏi một tập hợp các biểu thức được đánh giá.

getout <- TRUE 
# if getout==TRUE then the value of EXP, LOC, and FUN will be "OUTTA HERE"
# .... if getout==FALSE then it will be `3` for all these variables    

EXP <- eval(expression({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   }))

LOC <- local({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })

FUN <- (function(){
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })()

identical(EXP,LOC)
identical(EXP,FUN)

Hôm nay tôi đã tìm thấy một trường hợp mà người ta có thể thực sự cần return(ví dụ xấu xí của tôi ở trên là rất giả tạo): giả sử bạn cần kiểm tra xem một giá trị là NULLhay NA: trong những trường hợp này, trả về một chuỗi rỗng, nếu không trả về charactergiá trị. Nhưng một bài kiểm tra is.na(NULL)đưa ra một lỗi, vì vậy có vẻ như nó chỉ có thể được thực hiện if(is.null(x)) return("")và sau đó tiếp tục if(is.na(x)) ...... (Người ta có thể sử dụng length(x)==0thay vì is.null(x)nhưng vẫn không thể sử dụng length(x)==0 | is.na(x)nếu xNULL.)
lebatsnok

1
Đó là bởi vì bạn đã sử dụng |(vectơ HOẶC nơi cả hai bên được đánh giá) thay vì ||(ngắn mạch HOẶC, không vectơ, trong đó các vị từ được đánh giá lần lượt). Xem xét if (TRUE | stop()) print(1)so vớiif (TRUE || stop()) print(1)
asac

2

return có thể tăng khả năng đọc mã:

foo <- function() {
    if (a) return(a)       
    b     
}

3
Có lẽ nó có thể, tại đó. Nhưng nó không làm điều đó trong ví dụ của bạn. Thay vào đó, nó che khuất (hay đúng hơn là, làm phức tạp) dòng mã.
Konrad Rudolph

1
chức năng của bạn có thể được đơn giản hóa thành: foo <- function() a || b(IMO dễ đọc hơn; trong mọi trường hợp, không có khả năng đọc "thuần túy" nhưng dễ đọc theo ý kiến ​​của ai đó: có những người nói rằng ngôn ngữ lắp ráp hoàn toàn dễ đọc)
lebatsnok

1

Đối số của sự dư thừa đã đưa ra rất nhiều ở đây. Theo tôi đó không phải là lý do đủ để bỏ quareturn() . Dự phòng không tự động là một điều xấu. Khi được sử dụng một cách chiến lược, sự dư thừa làm cho mã rõ ràng hơn và dễ bảo trì hơn.

Xem xét ví dụ này: Các tham số chức năng thường có giá trị mặc định. Vì vậy, việc chỉ định một giá trị giống như mặc định là dư thừa. Ngoại trừ nó làm rõ ràng hành vi tôi mong đợi. Không cần phải kéo lên trang man chức năng để nhắc nhở bản thân mặc định là gì. Và không phải lo lắng về một phiên bản tương lai của chức năng thay đổi mặc định của nó.

Với một hình phạt hiệu suất không đáng kể để gọi return()(theo điểm chuẩn được đăng bởi những người khác ở đây), nó đi xuống theo phong cách chứ không phải đúng và sai. Đối với một cái gì đó là "sai", cần phải có một nhược điểm rõ ràng và không ai ở đây đã chứng minh thỏa đáng rằng bao gồm hoặc bỏ qua return()có một nhược điểm nhất quán. Có vẻ như trường hợp cụ thể và người dùng cụ thể.

Vì vậy, đây là nơi tôi đứng trên này.

function(){
  #do stuff
  ...
  abcd
}

Tôi không thoải mái với các biến "mồ côi" như trong ví dụ trên. Được abcdsẽ là một phần của một tuyên bố tôi đã không hoàn thành văn bản? Đây có phải là phần còn lại của một mối nối / chỉnh sửa trong mã của tôi và cần phải xóa không? Tôi đã vô tình dán / di chuyển một cái gì đó từ một nơi khác?

function(){
  #do stuff
  ...
  return(abdc)
}

Ngược lại, ví dụ thứ hai này cho tôi thấy rõ rằng đó là một giá trị trả về dự định, thay vì một số mã ngẫu nhiên hoặc không đầy đủ. Đối với tôi sự dư thừa này hoàn toàn không vô dụng.

Tất nhiên, một khi chức năng kết thúc và hoạt động tôi có thể loại bỏ trả lại. Nhưng loại bỏ nó tự nó là một bước bổ sung dư thừa, và theo quan điểm của tôi thì vô dụng hơn là bao gồm return()ở nơi đầu tiên.

Tất cả những gì đã nói, tôi không sử dụng return()trong các chức năng một lớp không tên ngắn. Ở đó, nó chiếm một phần lớn mã của hàm và do đó chủ yếu gây ra sự lộn xộn thị giác làm cho mã ít dễ đọc hơn. Nhưng đối với các hàm được định nghĩa và đặt tên chính thức lớn hơn, tôi sử dụng nó và có khả năng sẽ tiếp tục như vậy.


Có phải abcd sẽ là một phần của một tuyên bố tôi đã không viết xong? - Mặc dù vậy, nó khác với bất kỳ biểu thức nào khác mà bạn viết như thế nào? Điều này, tôi nghĩ là cốt lõi của sự bất đồng của chúng tôi. Có một biến đứng riêng có thể là đặc thù trong ngôn ngữ lập trình bắt buộc nhưng nó hoàn toàn bình thường và được mong đợi trong ngôn ngữ lập trình chức năng. Vấn đề, tôi khẳng định, chỉ đơn giản là bạn không quen thuộc với lập trình chức năng (thực tế là bạn nói về các câu lệnh của Cameron thay vì các biểu thức của ED, củng cố điều này).
Konrad Rudolph

Nó khác bởi vì mọi câu lệnh khác thường làm một cái gì đó theo một cách rõ ràng hơn: Đó là một bài tập, so sánh, gọi hàm ... Có các bước mã hóa đầu tiên của tôi là bằng các ngôn ngữ bắt buộc và tôi vẫn sử dụng các ngôn ngữ bắt buộc. Có tín hiệu hình ảnh thống nhất trên các ngôn ngữ (bất cứ nơi nào ngôn ngữ cho phép) giúp công việc của tôi dễ dàng hơn. A return()trong R không có gì. Đó là sự dư thừa khách quan, nhưng "vô dụng" là đánh giá chủ quan của bạn. Dư thừa và vô dụng không nhất thiết phải là từ đồng nghĩa. Đó là nơi chúng tôi không đồng ý.
cymon

Ngoài ra, tôi không phải là kỹ sư phần mềm cũng như nhà khoa học máy tính. Đừng đọc quá nhiều sắc thái trong việc sử dụng thuật ngữ của tôi.
cymon

Chỉ cần làm rõ: Red Redplus và vô dụng không nhất thiết phải là từ đồng nghĩa. Đó là nơi chúng tôi không đồng ý. - Không, tôi hoàn toàn đồng ý với điều đó, và tôi đã nêu rõ quan điểm đó trong câu trả lời của mình. Dự phòng có thể hữu ích hoặc thậm chí quan trọng . Nhưng điều này cần phải được tích cực thể hiện, không được giả định. Tôi hiểu lý lẽ của bạn về lý do tại sao bạn nghĩ điều này đúng return, và mặc dù tôi không tin rằng tôi nghĩ nó có khả năng (nó chắc chắn là một ngôn ngữ bắt buộc. Tôi tin rằng nó không dịch sang các ngôn ngữ chức năng).
Konrad Rudolph
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.