Явно вызывая return в функции или нет

Некоторое время назад Саймон Урбанек (Simon Urbanek) из основной команды R (я полагаю) сделал мне замечание за то, что он рекомендовал пользователю явно вызывать return в конце функции (хотя его комментарий был удален):

foo = function() {
  return(value)
}

вместо этого он рекомендовал:

foo = function() {
  value
}

Вероятно, в такой ситуации требуется:

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

Его комментарий пролил некоторый свет на то, почему не звонить return если это строго не нужно, это хорошо, но это было удалено.

Мой вопрос: почему не звонит return быстрее или лучше, а значит предпочтительнее?

7 ответов

Решение

Вопрос заключался в следующем: почему (явно) вызов return не является более быстрым или лучшим, и, следовательно, предпочтительным?

В документации R нет формулировок, делающих такое предположение.
На странице руководства?'Функция' говорит:

function( arglist ) expr
return(value)

Это быстрее без вызова возврата?

И то и другое function() а также return() примитивные функции и function() сам возвращает последнее оцененное значение, даже не включая return() функция.

призвание return() как .Primitive('return') с этим последним значением в качестве аргумента будет выполнять ту же работу, но требуется еще один вызов. Так что это (часто) ненужное .Primitive('return') Вызов может привлечь дополнительные ресурсы. Простое измерение, однако, показывает, что результирующая разница очень мала и, следовательно, не может быть причиной отказа от явного возврата. Следующий график создается из данных, выбранных таким образом:

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

Функция сравнения прошедшего времени

Изображение выше может немного отличаться на вашей платформе. Основываясь на измеренных данных, размер возвращаемого объекта не вызывает никакой разницы, количество повторений (даже в увеличенном масштабе) имеет очень маленькое различие, которое в реальном слове с реальными данными и реальным алгоритмом не может быть подсчитано или произведено. скрипт работает быстрее.

Лучше без звонка вернуть?

Return является хорошим инструментом для четкого проектирования "листьев" кода, где процедура должна заканчиваться, выпрыгивать из функции и возвращать значение.

# 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
> 

От стратегии и стиля программирования программиста зависит, какой стиль он использует, он не может использовать return(), так как это не требуется.

R основные программисты используют оба подхода, т.е. с и без явного возврата (), как это можно найти в источниках "базовых" функций.

Много раз используется только return() (без аргументов), возвращая NULL в тех случаях, чтобы условно остановить функцию.

Не ясно, лучше это или нет, так как обычный пользователь или аналитик, использующий R, не видит реальной разницы.

Мое мнение таково, что вопрос должен быть следующим: существует ли какая-либо опасность в использовании явного возврата из реализации R?

Или, может быть, лучше, пользовательский код функции должен всегда спрашивать: каков эффект от использования явного возврата (или размещения объекта, который будет возвращен как последний лист ветви кода) в коде функции?

Если все согласны с этим

  1. return не требуется в конце тела функции
  2. не используется return немного быстрее (согласно тесту @Alan, 4,3 микросекунды против 5,1)

мы все должны прекратить использование return в конце функции? Я конечно не буду, и я хотел бы объяснить почему. Я надеюсь услышать, если другие люди разделяют мое мнение. И я прошу прощения, если это не прямой ответ на ФП, а скорее как длинный субъективный комментарий.

Моя главная проблема с неиспользованием return в том, что, как указывал Павел, в теле функции есть и другие места, где она может вам понадобиться. И если вы вынуждены использовать return где-то в середине вашей функции, почему бы не сделать все return заявления явные? Я ненавижу быть непоследовательным. Также я думаю, что код читается лучше; можно отсканировать функцию и легко увидеть все точки выхода и значения.

Павел использовал этот пример:

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

К сожалению, можно отметить, что это может быть легко переписано как:

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

Последняя версия даже соответствует некоторым стандартам программирования, которые поддерживают один оператор возврата на функцию. Я думаю, что лучший пример мог бы быть:

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

Это было бы гораздо сложнее переписать с помощью одного оператора return: потребовалось бы несколько breaks и сложная система булевых переменных для их распространения. Все это говорит о том, что правило одиночного возврата не очень хорошо работает с R. Так что если вы собираетесь использовать return в некоторых местах тела вашей функции, почему бы не быть последовательным и использовать его везде?

Я не думаю, что аргумент скорости является действительным. Разница в 0,8 микросекунды - ничто, когда вы начинаете смотреть на функции, которые действительно что-то делают. Последнее, что я вижу, это то, что он печатает меньше, но я не ленивый.

Это интересная дискуссия. Я думаю, что пример @flodel превосходен. Тем не менее, я думаю, что это иллюстрирует мою точку зрения (и @koshke упоминает об этом в комментарии), что returnимеет смысл, когда вы используете императив вместо функционального стиля кодирования.

Не в последнюю очередь, но я бы переписал foo как это:

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

Функциональный стиль позволяет избежать изменений состояния, например, хранить значение output, В этом стиле return не к месту; foo выглядит больше как математическая функция.

Я согласен с @flodel: использование сложной системы логических переменных в bar было бы менее понятно, и бессмысленно, когда у вас есть return, Что делает bar так поддается return Заявления о том, что он написан в императивном стиле. Действительно, булевы переменные представляют изменения "состояния", которых избегают в функциональном стиле.

Это действительно сложно переписать bar в функциональном стиле, потому что это просто псевдокод, но идея примерно такая:

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.
}

while цикл будет сложнее всего переписать, потому что он контролируется изменениями состояния в a,

Потеря скорости, вызванная вызовом return незначительно, но эффективность, полученная благодаря return и переписывание в функциональном стиле часто огромно. Говорить новым пользователям прекратить использование return вероятно, не поможет, но приведение их к функциональному стилю окупится.


@Павел return необходимо в императивном стиле, потому что вы часто хотите выйти из функции в разных точках цикла. Функциональный стиль не использует циклы, и поэтому не нуждается return, В чисто функциональном стиле последний вызов почти всегда является желаемым возвращаемым значением.

В Python функции требуют return заявление. Однако, если вы запрограммировали свою функцию в функциональном стиле, у вас, скорее всего, будет только один return утверждение: в конце вашей функции.

Используя пример из другого поста Stackru, допустим, нам нужна функция, которая возвращает TRUE если все значения в данном x имел странную длину. Мы могли бы использовать два стиля:

# 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)

В функциональном стиле возвращаемое значение естественным образом попадает в конец функции. Опять же, это больше похоже на математическую функцию.

@GSee предупреждения, изложенные в ?ifelse безусловно, интересны, но я не думаю, что они пытаются отговорить использование этой функции. По факту, ifelse имеет преимущество автоматической векторизации функций. Например, рассмотрим слегка измененную версию foo:

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

Эта функция прекрасно работает, когда length(a) есть 1. Но если переписать foo с ifelse

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

Сейчас foo работает на любой длине a, На самом деле, это будет работать даже когда a это матрица Возвращает значение той же формы, что и test это функция, которая помогает с векторизацией, а не проблема.

Кажется что без return() это быстрее...

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

____ РЕДАКТИРОВАТЬ __ _ __ _ __ _ __ _ __ _ ___

Перехожу к другим ориентирам (benchmark(fuu(x),foo(x),replications=1e7)) и результат обратный... Я попробую на сервере.

У меня вопрос: почему не звонит return Быстрее

Это быстрее, потому что returnявляется (примитивной) функцией в R, а это означает, что ее использование в коде требует затрат на вызов функции. Сравните это с большинством других языков программирования, гдеreturn является ключевым словом, но не вызовом функции: оно не переводится ни в какое исполнение кода времени выполнения.

Тем не менее, вызов примитивной функции в R выполняется довольно быстро, а вызов returnнесет незначительные накладные расходы. Это не аргумент в пользу исключенияreturn.

или лучше, и поэтому предпочтительнее?

Потому что нет причин его использовать.

Потому что это избыточно и не добавляет полезной избыточности.

Чтобы было ясно: избыточность иногда может быть полезной. Но в большинстве случаев избыточность не такого рода. Вместо этого он добавляет визуальный беспорядок без добавления информации: это программный эквивалент слова-заполнителя или диаграммы).

Consider the following example of an explanatory comment, which is universally recognised as bad redundancy because the comment merely paraphrases what the code already expresses:

# Add one to the result
result = x + 1

Using return in R falls in the same category, because R is a functional programming language, and in R every function call has a value. This is a fundamental property of R. And once you see R code from the perspective that every expression (including every function call) has a value, the question then becomes: “why should I use return?” There needs to be a positive reason, since the default is not to use it.

One such positive reason is to signal early exit from a function, say in a guard clause:

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

This is a valid, non-redundant use of return. However, such guard clauses are rare in R compared to other languages, and since every expression has a value, a regular if does not require return:

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

We can even rewrite f like this:

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

… where if (cond) expr is the same as if (cond) expr else NULL.

Finally, I’d like to forestall three common objections:

  1. Some people argue that using return adds clarity, because it signals “this function returns a value”. But as explained above, every function returns something in R. Thinking of return as a marker of returning a value isn’t just redundant, it’s actively misleading.

  2. Relatedly, the Zen of Python has a marvellous guideline that should always be followed:

    Explicit is better than implicit.

    How does dropping redundant returnне нарушать это? Потому что возвращаемое значение функции на функциональном языке всегда явно: это ее последнее выражение. Это снова тот же аргумент о явности и избыточности.

    Фактически, если вам нужна явность, используйте ее, чтобы выделить исключение из правила: отметьте функции, которые не возвращают значимого значения, которые вызываются только для их побочных эффектов (например,cat). За исключением того, что у R маркер лучше, чем уreturn для этого случая: invisible. Например, я бы написал

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
    
  3. Но как насчет длинных функций? Не будет ли легко потерять из виду то, что возвращается?

    Два ответа: во-первых, не совсем. Правило ясное: последнее выражение функции - это ее значение. Не за чем следить.

    Но что более важно, проблема длинных функций заключается не в отсутствии явного returnмаркеры. Это длина функции. Длинные функции почти (?) Всегда нарушают принцип единой ответственности, и даже если они этого не делают, они выиграют, если будут разбиты на части для удобства чтения.

Проблема, заключающаяся в том, чтобы не ставить "return" явно в конце, заключается в том, что если добавить один оператор в конец метода, внезапно возвращаемое значение будет неправильным:

foo <- function() {
    dosomething()
}

Это возвращает значение dosomething(),

Теперь мы идем на следующий день и добавляем новую строку:

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

Мы хотели, чтобы наш код возвращал значение dosomething(), но вместо этого это больше не делает.

С явным возвратом это становится действительно очевидным:

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

Мы можем видеть, что в этом коде есть что-то странное, и исправить это:

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

Я думаю о return как трюк. Как правило, значение последнего выражения, вычисленного в функции, становится значением функции - и этот общий шаблон встречается во многих местах. Все следующие оценки до 3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

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

Какие return В действительности не возвращает значение (это делается с ним или без него), а "прерывает" функцию нерегулярным способом. В этом смысле это самый близкий эквивалент оператора GOTO в R (есть также break и next). я использую return очень редко и никогда в конце функции.

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

... это можно переписать как if(a) a else b который намного лучше читается и менее вьющийся. Нет необходимости return вообще здесь. Мой опытный случай использования "возврата" будет что-то вроде...

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")
   }

Как правило, потребность во многих возвратах предполагает, что проблема либо уродлива, либо плохо структурирована.

<>

return на самом деле не нужна функция для работы: вы можете использовать ее, чтобы вырваться из набора выражений для оценки.

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)

Здесь часто упоминается аргумент избыточности. На мой взгляд, этого недостаточно, чтобы не упоминатьreturn(). Избыточность автоматически не является плохой вещью. При стратегическом использовании избыточность делает код более понятным и удобным.

Рассмотрим следующий пример: параметры функции часто имеют значения по умолчанию. Таким образом, указание значения, аналогичного значению по умолчанию, является избыточным. За исключением того, что это делает очевидным поведение, которого я ожидаю. Не нужно открывать страницу справки по функциям, чтобы напомнить себе, какие настройки установлены по умолчанию. И не беспокойтесь о том, что будущая версия функции изменит свои значения по умолчанию.

С незначительной потерей производительности за вызов return()(согласно тестам, опубликованным здесь другими), все сводится к стилю, а не к правильному и неправильному. Чтобы что-то было "неправильным", должен быть явный недостаток, и никто из здесь не продемонстрировал удовлетворительно, что включение или исключениеreturn()имеет постоянный недостаток. Это кажется очень специфичным для конкретного случая и конкретного пользователя.

Итак, вот где я стою на этом.

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

Меня не устраивают "сиротские" переменные, как в примере выше. Былabcdбудет частью заявления, которое я не закончил писать? Это остаток склейки / редактирования в моем коде, и его нужно удалить? Я случайно вставил / переместил что-то откуда-то еще?

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

Напротив, этот второй пример показывает мне, что это предполагаемое возвращаемое значение, а не какой-то случайный или неполный код. Для меня эта избыточность совершенно не бесполезна.

Конечно, как только функция будет завершена и заработает, я могу удалить возврат. Но его удаление само по себе является лишним шагом, и, на мой взгляд, более бесполезным, чем включениеreturn() на первом месте.

Все сказанное я не использую return()короче безымянные однострочные функции. Там он составляет большую часть кода функции и, следовательно, в основном вызывает визуальный беспорядок, который делает код менее читаемым. Но для более крупных формально определенных и именованных функций я использую его и, вероятно, продолжу так же.

return может повысить читаемость кода:

foo <- function() {
    if (a) return(a)       
    b     
}
Другие вопросы по тегам