R PROGRAMMING • FUNCTIONS AND PROGRAM STRUCTURE

Defining Functions — Define functions with function() and return values

Master how to encapsulate reusable logic in R by defining custom functions and controlling their return behavior.

Historical Context & Motivation

The notion of a function as a reusable, parameterized unit of computation predates modern programming languages by centuries, with roots in mathematical abstraction. In the context of programming, functions became first-class constructs during the development of Lisp in the late 1950s, which treated functions as data that could be passed, returned, and composed. R inherits this functional programming tradition through its lineage from S, a language designed at Bell Labs for statistical computing and interactive data analysis. Understanding this heritage helps explain why R treats functions as first-class objects and why the function() keyword occupies such a central role in the language.

1958
Lisp and Lambda Calculus in Computing
John McCarthy's Lisp introduced the concept of functions as first-class citizens in programming, directly inspired by Alonzo Church's lambda calculus. This paradigm established the theoretical foundation for user-defined functions in all subsequent functional and multi-paradigm languages.
1976
S Language at Bell Labs
John Chambers and colleagues developed S as an interactive environment for statistics. S allowed users to define custom functions using a syntax remarkably close to what R uses today, democratizing extensibility in statistical computing.
1993
R's Birth at the University of Auckland
Ross Ihaka and Robert Gentleman created R as a free, open-source implementation of S. They preserved S's function-centric design while adding lexical scoping rules inspired by Scheme, making closures and nested function definitions natural.
2000
R 1.0 and CRAN Launch
The release of R 1.0 and the Comprehensive R Archive Network (CRAN) formalized the package ecosystem, where every contributed package is built entirely on user-defined functions. The function became the universal unit of abstraction in R.

Without the ability to define custom functions, R programmers would be confined to copy-pasting repetitive code blocks, making scripts brittle, error-prone, and nearly impossible to maintain. The core question this lesson addresses is straightforward yet fundamental: how do we encapsulate a sequence of operations into a named, reusable unit that accepts inputs and produces a well-defined output?

Core Principles of Function Definition

Defining a function in R involves three conceptual components that map directly onto the language syntax: the formal parameters (inputs the function expects), the body (the sequence of expressions that perform computation), and the return value (the result passed back to the caller). In R, functions are themselves objects—they can be assigned to variables, stored in lists, and passed as arguments to other functions. This first-class status is central to idiomatic R programming and distinguishes it from languages where functions are merely procedural blocks.

1

The function() Constructor

The keyword function() creates a new function object. Its parentheses enclose formal arguments, and the body follows in curly braces. The result is assigned to a name via <-.
2

Implicit vs. Explicit Return

R functions return the value of the last evaluated expression by default (implicit return). The return() function provides explicit early exit. Understanding when to use each is a matter of style and control flow clarity.
3

Default Argument Values

Formal parameters can be given default values using arg = default syntax. These defaults are evaluated lazily—at call time, not at definition time—which allows defaults to depend on other arguments.
4

Lexical Scoping

R uses lexical scoping: a function's body can access variables from the environment in which the function was defined, not where it is called. This enables closures—functions that capture and carry their enclosing environment.
5

Functions as First-Class Objects

A function in R is an object of type closure. It can be stored in a variable, placed in a list, passed as an argument to higher-order functions like lapply(), or returned from another function.
KEY TAKEAWAY
Think of defining a function like writing a recipe card in a kitchen. The ingredients list corresponds to your formal parameters, the preparation steps correspond to the function body, and the finished dish is the return value. Once the recipe card exists, anyone can follow it with different ingredients—just as once a function is defined, it can be called with different arguments to produce different results without rewriting the logic.

Anatomy of an R Function

The following diagram dissects the anatomy of an R function definition, labeling each syntactic component and illustrating the data flow from input arguments through the function body to the return value. Pay close attention to how the assignment operator binds the function object to a name, and how the return mechanism works both implicitly and explicitly.

The diagram shows the three structural components of an R function definition: function name (①), the function() keyword (②), and formal parameters (③). The lower portion traces data flow from argument binding through body evaluation to the return value, with a comparison of implicit versus explicit return semantics.

Notice that the assignment operator <- binds the function object—created by function()—to the name square_sum. The curly braces delimit the body, and the value of the last expression evaluated within those braces becomes the implicit return value. When explicit control flow is needed—such as returning early from a conditional branch—the return() function terminates execution and sends the specified value back to the caller. Both mechanisms are semantically equivalent when placed at the end of the body, but idiomatic R style typically prefers implicit return for conciseness.

How R Functions Work Under the Hood

To fully grasp function behavior in R, it helps to understand the evaluation model. When you call a function, R creates a new evaluation environment—a frame on the call stack—where formal parameters are bound to the supplied arguments. R uses lazy evaluation for function arguments: an argument is not evaluated until it is actually used inside the body. This is distinct from languages like C or Java, which evaluate all arguments eagerly before entering the function.

The Three Components of a Function Object

Internally, every R function object (of type closure) consists of three parts that you can inspect programmatically. The formals() function returns a named list of the function's formal parameters and their default values. The body() function returns the unevaluated expression constituting the function body. Finally, environment() returns the enclosing environment—the environment where the function was defined, which is the starting point for lexical scoping lookups. These three components together fully specify the function's behavior.

FUNCTION OBJECT STRUCTURE
f = (formals, body, environment)
Every R closure is a triple: formals = named list of parameters with defaults, body = unevaluated R expression, environment = the enclosing scope at definition time.

Return Value Semantics

R functions always return exactly one object. If you need to return multiple values, you must wrap them in a composite structure—typically a list() or a named list. The invisible() function is a variant of return that suppresses automatic printing when the result is not assigned; you see this in functions like print() and plot() which return their input invisibly for chaining. Understanding these return semantics is essential for writing well-behaved functions that integrate smoothly into pipelines and higher-order function calls.

RETURN VALUE RULES
return value = last expression evaluated OR return(expr) OR invisible(expr)
The three mechanisms for producing return values. return() causes immediate exit; invisible() suppresses auto-printing of the returned value.
Common Pitfall
If the last expression in your function body is an assignment (e.g., result <- x + y), the function will return the value invisibly. This can cause confusing behavior when you expect the result to print. Either end with the variable name alone (result) or use return(result) to be explicit.

Function Definition Patterns & Argument Handling

R provides a rich set of features for managing function arguments, and understanding the common patterns will make your function definitions more flexible and robust. This section examines default arguments, the ellipsis (...) mechanism for variadic functions, positional versus named argument matching, and several idiomatic patterns used throughout the R ecosystem.

This diagram illustrates four call patterns for a function with a default argument, and demonstrates the ellipsis (...) pattern for accepting a variable number of arguments, which is ubiquitous in base R functions.
Common argument patterns in R function definitions
PatternSyntax ExampleUse Case
No default, required argumentfunction(x)When the caller must always supply a value
Default valuefunction(x, n = 10)Providing sensible defaults to simplify common usage
Ellipsis passthroughfunction(...) paste(...)Wrapper functions forwarding arguments to inner functions
NULL sentinelfunction(x, label = NULL)Optional argument where presence is checked via is.null()
missing() checkif (missing(x)) stop("x required")Distinguishing between a default value and an explicitly passed one

Worked Example: Building a Summary Statistics Function

Let us walk through the design and implementation of a custom function that computes summary statistics for a numeric vector. This example demonstrates parameter definition, default values, input validation, and returning multiple values via a named list.

Creating describe_vector()
1
Step 1 — Define the SignatureWe begin by defining the function name and its formal parameters. The function will accept a numeric vector x and an optional logical argument na.rm defaulting to TRUE.
describe_vector <- function(x, na.rm = TRUE)
2
Step 2 — Validate InputsInside the body, we first check that x is numeric. If not, we call stop() to halt execution with an informative error message. This is a best practice known as a guard clause.
if (!is.numeric(x)) stop("x must be numeric")
3
Step 3 — Compute StatisticsWe compute the mean, standard deviation, median, and range, passing the na.rm argument to each summary function so that NA handling is consistent and user-controlled.
m <- mean(x, na.rm = na.rm); s <- sd(x, na.rm = na.rm); md <- median(x, na.rm = na.rm); r <- range(x, na.rm = na.rm)
4
Step 4 — Return a Named ListSince R functions return a single object, we bundle all computed statistics into a named list. The list is the last expression in the body, so it serves as the implicit return value.
list(mean = m, sd = s, median = md, min = r[1], max = r[2])
5
Step 5 — Test the FunctionWe call describe_vector(c(4, 7, 2, NA, 9)) and verify the output. Because na.rm = TRUE by default, the NA is stripped before computation, yielding mean = 5.5, sd ≈ 3.11, median = 5.5, min = 2, max = 9.
$mean [1] 5.5 $sd [1] 3.109126 $median [1] 5.5 $min [1] 2 $max [1] 9
💻 Complete Code
describe_vector <- function(x, na.rm = TRUE) { if (!is.numeric(x)) stop("x must be numeric") m <- mean(x, na.rm = na.rm) s <- sd(x, na.rm = na.rm) md <- median(x, na.rm = na.rm) r <- range(x, na.rm = na.rm) list(mean = m, sd = s, median = md, min = r[1], max = r[2]) }

Implicit vs. Explicit Return — Style and Pitfalls

One of the most frequently debated topics in the R community is whether to use return() explicitly or rely on implicit return. Both approaches are correct, but they carry different tradeoffs in terms of readability, safety, and adherence to community conventions. The following table compares the two approaches across several dimensions.

Comparison of implicit and explicit return strategies in R
DimensionImplicit ReturnExplicit return()
ConcisenessMore concise; the last expression is the return value naturallyAdds one function call but makes intent unambiguous
ReadabilityNatural for experienced R programmers; matches functional styleClearer for newcomers or polyglot teams familiar with C/Java
Early exitCannot exit early from a branch without return()Essential for guard clauses and conditional early termination
PerformanceMarginally faster (no function call overhead), but negligible in practiceTrivial overhead; not a meaningful consideration
Community conventionPreferred by tidyverse and Google R style guidesUsed in some legacy codebases and Bioconductor packages
PRACTICAL GUIDELINE
Use implicit return when the function body flows linearly to a single final expression—this is analogous to how a mathematical function maps inputs to outputs without side-channel exits. Use explicit return() when you need to exit early from conditional branches, where the alternative would be deeply nested if-else structures. Think of it like a circuit breaker in engineering: the normal path runs to completion, but exceptional conditions trip the breaker early.

Connection to Advanced Functional Programming

The ability to define functions is the gateway to R's powerful functional programming capabilities. Once you understand the basic function() construct, you are positioned to explore closures (functions that remember their defining environment), higher-order functions (functions that take other functions as arguments or return them), and function factories (functions whose return value is itself a new function). These concepts build directly on the mechanics covered in this lesson.

From basic function definition to advanced functional programming concepts
ConceptThis LessonAdvanced Extension
Function definitionf <- function(x) x + 1Anonymous functions: \(x) x + 1 (R 4.1+ shorthand)
Return valuesReturning data (vectors, lists)Returning functions (function factories, memoization wrappers)
ScopingFunction body accesses enclosing environmentClosures that capture and mutate enclosing state via <<-
Argument handlingDefaults, named args, ...Non-standard evaluation (NSE) via substitute(), rlang::enquo()
Passing functionsCalling built-in functions inside custom onesHigher-order functions: lapply(), purrr::map(), custom functionals

As you progress in R, you will discover that nearly every advanced technique—from building custom packages to implementing S3/S4 method dispatch—relies on the same function() construct introduced here. Mastering the basics of function definition and return values provides the scaffolding upon which the entire R programming paradigm is built. The R 4.1 shorthand \(x) x + 1 is syntactic sugar for function(x) x + 1, further underscoring that everything in R ultimately flows through the same function object mechanism.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain the difference between implicit and explicit return in R. Given the function f <- function(x) { y <- x * 2; y } and g <- function(x) { y <- x * 2 }, what does each return when called with f(5) and g(5)? Will both print a result to the console?
PROBLEM 2BASIC
Write an R function called circle_area that takes a radius r as input and returns the area of a circle (πr²). Test it with r = 5.
PROBLEM 3INTERMEDIATE
Write a function safe_log that takes arguments x and base = exp(1). If x is not numeric or contains non-positive values, the function should return NA with a warning. Otherwise, return log(x, base). Use explicit return for the early-exit cases.
PROBLEM 4APPLIED
A researcher needs a function z_score that standardizes a numeric vector by subtracting its mean and dividing by its standard deviation. The function should accept an optional na.rm argument (defaulting to FALSE) and return the standardized vector. Write this function and apply it to c(10, 20, 30, 40, 50).
PROBLEM 5CRITICAL THINKING
Consider the function: make_adder <- function(n) { function(x) x + n }. Explain what add5 <- make_adder(5) creates, what add5(3) returns, and how this relates to the concepts of closures and lexical scoping discussed in this lesson. Why does the inner function still know the value of n even though make_adder has already finished executing?

Summary

In R, functions are defined using the function() keyword, which creates a first-class closure object comprising three parts: formals (parameters with optional default values), a body of R expressions, and an enclosing environment that enables lexical scoping. Functions return the value of their last evaluated expression (implicit return) or can exit early via return() (explicit return).

Key argument patterns include default values for optional parameters, the ellipsis (...) for variadic argument passing, and named argument matching for flexible calling conventions. To return multiple values, wrap them in a list(). These fundamentals connect directly to advanced topics like closures, higher-order functions, and function factories—making function definition the single most important skill in R programming.

Varsity Tutors • R Programming • Defining Functions — Define functions with function() and return values