R Programming • Syntax and Core Types

Naming Conventions — Use basic naming conventions and avoid masking functions

Write clearer, more maintainable R code by choosing descriptive names and avoiding collisions with built-in functions.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

R traces its lineage to the S language, developed at Bell Labs in the 1970s by John Chambers and colleagues. S was designed to be an interactive, exploratory data-analysis environment, and its developers chose a remarkably permissive approach to naming: users could create objects with virtually any identifier, including names already occupied by built-in functions. When Ross Ihaka and Robert Gentleman created R in the early 1990s at the University of Auckland, they inherited this permissive scoping model. The flexibility that made S and R appealing for rapid statistical exploration simultaneously introduced a persistent class of bugs—function masking—where a user-defined object silently shadows a base function, causing downstream code to fail in subtle and often hard-to-diagnose ways.

As R's user base expanded from statisticians to software engineers and data scientists, the community recognized that a shared set of naming conventions was essential for collaboration, package interoperability, and code review. The tidyverse style guide, Hadley Wickham's formalization of best practices, became a de-facto standard around 2014, but discussions around naming discipline date back to the earliest S-PLUS documentation. The timeline below traces the key moments that shaped modern R naming conventions.

1976 • S Language at Bell Labs

John Chambers and Rick Becker create S, introducing a permissive symbol-binding model where any identifier—including those of built-in functions—can be reassigned by the user.

1993 • R is Born

Ihaka and Gentleman release R, inheriting S's scoping rules. The dynamic, lexically scoped environment makes masking easy to trigger accidentally.

2004 • Google's R Style Guide

Google publishes one of the first widely circulated corporate R style guides, recommending dot-separated names for variables and UpperCamelCase for functions.

2014 • Tidyverse Style Guide

Hadley Wickham codifies snake_case as the preferred convention for both functions and variables, and explicitly warns against masking base-R function names.

2020 • lintr & styler Ecosystem

Static analysis tools like lintr and automated formatters like styler integrate naming-convention checks directly into CI/CD pipelines, making enforcement automatic.

The central question these developments address is straightforward yet critical: how should an R programmer name objects so that code is readable, portable across projects, and free from the insidious bugs that arise when user-defined names collide with the language's own vocabulary?

SECTION 2

Core Principles & Definitions

Effective naming in R rests on a small set of principles that, taken together, eliminate the most common sources of confusion and error. These principles apply whether you are writing a one-off analysis script or developing a CRAN package. The concept grid below distills them into actionable rules.

1

Use snake_case Consistently

Separate words with underscores and use all lowercase letters. This convention (my_variable) is endorsed by the tidyverse style guide and is the dominant style in modern R.
2

Be Descriptive, Not Terse

Choose names that convey meaning: student_count over sc. A reader should understand the object's purpose without looking at its assignment.
3

Never Mask Built-in Functions

Avoid assigning to names like c, mean, data, or df. Masking hides the original function behind your object in the search path, leading to cryptic runtime errors.
4

Nouns for Objects, Verbs for Functions

Name data objects with nouns (sales_data) and functions with verbs (compute_tax). This makes the role of each symbol immediately evident at the call site.
5

Avoid Dots in Non-S3 Names

The dot (.) in R has special meaning in S3 method dispatch (print.lm). Using dots in ordinary variable names (my.var) creates ambiguity about whether the object is an S3 method.
✦

KEY TAKEAWAY

Think of R's namespace like a crowded library. Every book (function) has a shelf (environment). If you bring your own book labeled mean and place it on the nearest shelf, the librarian (R's search path) will grab yours first when someone asks for "mean"—even though the official statistics textbook is right behind it. Choosing unique, descriptive names is like giving your books distinct labels so they never get confused with the library's own collection.

SECTION 3

Visual Explanation — The Search Path and Masking

Understanding why masking occurs requires a mental model of R's environment search path. When you type a name at the console, R searches through a chain of environments—from the global environment down through attached packages to base R—and returns the first match it finds. The diagram below illustrates what happens when a user creates an object named mean in the global environment.

R Environment Search Path — Masking in ActionGlobal Environmentmean <- 42student_count <- 30⚠ MASKS base::meansearch()package:dplyrfilter(), mutate(), ...package:statslm(), t.test(), ...package:basemean(), c(), sum(), ...What happens when you call mean(x)?1. R looks in Global Env → finds422. Returns 42 (a number, not a function)3. Error: "could not find function 'mean'"✓ Fix: use avg_score instead of meanAfter renaming — No maskingavg_score <- 42mean(x) → searches to base → ✓ worksbase::mean is accessible as expected
The diagram shows how R traverses the search path from the Global Environment down to package:base. When a user-defined object shares a name with a base function, R finds the user object first and never reaches the original function.

Notice that the search path is linear and ordered. The first match wins rule is what makes masking so dangerous: R will never warn you that a user-defined object with the same name as a function exists, unless you explicitly run conflicts() or use a linter. The fix shown on the right panel—renaming mean to avg_score—is descriptive, avoids the collision, and makes the code immediately self-documenting.

SECTION 4

How R Resolves Names — The Scoping Mechanism

R uses lexical scoping (also called static scoping), which means that the binding of a free variable in a function is determined by the environment in which the function was defined, not where it is called. This rule interacts with the search path to produce masking behavior. When you assign a value to a name in the global environment, that name enters the first environment on the search path. Any subsequent lookup for that name—whether by your own code, a library function's internal call, or an interactive command—will resolve to your object before reaching the package namespace that originally defined the function.

The Lookup Algorithm

When R encounters a symbol f in a function-call position (e.g., f(x)), it performs function lookup: it walks the search path looking specifically for an object of mode function. If the masking object is also a function, R will call it—potentially with entirely wrong semantics. If the masking object is not a function (e.g., a numeric vector), R skips it during function lookup and may still find the original, but only in certain contexts. For bare symbol evaluation (e.g., print(mean)), R does a general lookup that stops at the first match regardless of mode.

⚠

Subtle Trap: Function vs. Value Masking

If you write c <- 10, calling c(1, 2, 3) still works because R's function lookup skips non-function objects. However, code that passes c as an argument (e.g., sapply(list_of_vecs, c)) will pass the number 10, not the concatenation function. This inconsistency is a major source of hard-to-find bugs.

The conflicts() and find() Diagnostic Tools

R provides built-in functions to detect masking. Calling conflicts(detail = TRUE) returns a named list showing every symbol that exists in more than one environment on the search path. Similarly, find("mean") will return every environment containing a binding for mean, letting you see whether ".GlobalEnv" appears first. When masking is suspected, base::mean(x) explicitly qualifies the namespace and bypasses the search path entirely. While this is a valid emergency fix, the real solution is to rename the offending object so the collision never occurs.

SECTION 5

Naming Styles in R — A Detailed Comparison

The R ecosystem historically tolerated several competing naming conventions, which can create confusion when reading code from different authors or packages. Understanding the landscape helps you make informed, consistent choices and read legacy code fluently. The diagram below presents the four major styles side by side, along with their prevalence and recommended usage.

R Naming Conventions — Comparison MatrixStyleExamplePrevalenceNotessnake_caseTidyverse standardstudent_countcompute_grade()~90% modern R✓ RecommendedReadable, unambiguouscamelCaseShiny / some BioconductorstudentCountcomputeGrade()~15% (Shiny)AcceptableFamiliar to Java/JS devsdot.separatedLegacy S / early Rstudent.countcompute.grade()~5% legacy⚠ AvoidConflicts with S3 dispatchALLCAPS / single letterMath notation, throwawayN, x, df, SCF(), Tvaries✗ DangerousT/F mask TRUE/FALSE, df masks densityBar lengths approximate relative prevalence in CRAN packages (2024 survey data)
The four major naming styles in R, showing snake_case as the dominant modern convention. Note how dot.separated names create ambiguity with S3 method dispatch, and single-letter names risk masking built-in constants like T and F.

Commonly Masked Names to Avoid

Dangerous NameBuilt-in It MasksSafe Alternative
cBase concatenation function c()count, combo
meanArithmetic mean base::mean()avg_score, mean_val
dataDataset loader utils::data()raw_data, survey_df
dfF-distribution density stats::df()my_df, results_df
T / FAbbreviations for TRUE / FALSEAlways spell out TRUE / FALSE
sumSummation base::sum()total, running_sum
listList constructor base::list()item_list, param_list

Frequently masked names with safe snake_case replacements

SECTION 6

Worked Example — Refactoring a Messy Script

Consider a script written by a novice R programmer who wants to compute class statistics. The original code violates multiple naming conventions and introduces masking bugs. We will walk through identifying and fixing each issue.

⚠

Original (Buggy) Code

data <- c(88, 92, 75, 96, 84) # masks utils::data() mean <- sum(data) / length(data) # masks base::mean() T <- mean > 85 # masks TRUE df <- data.frame(scores = data, pass = T) # masks stats::df() print(df)

Refactoring Step by Step

  1. 1

    Step 1 — Identify All Masked Names

    Run conflicts(detail = TRUE) or manually inspect each assignment. The problematic names are: data (masks utils::data), mean (masks base::mean), T (masks TRUE), and df (masks stats::df).
    4 masking violations identified
  2. 2

    Step 2 — Choose Descriptive snake_case Replacements

    Replace each name: data → exam_scores (noun, describes the content); mean → avg_score (descriptive, no collision); T → is_passing (Boolean prefix convention); df → results_df (adds qualifier).
    All names are now unique and self-documenting
  3. 3

    Step 3 — Use Built-in Functions Instead of Manual Computation

    Now that mean is no longer masked, we can replace the manual formula sum(data) / length(data) with the idiomatic call mean(exam_scores). This is both clearer and more robust, since base::mean() handles edge cases like NA values with its na.rm argument.
    avg_score <- mean(exam_scores)
  4. 4

    Step 4 — Spell Out TRUE/FALSE Explicitly

    The original code used T as a variable, but even in contexts where T is used for the Boolean constant, always write TRUE and FALSE in full. Unlike these reserved words, T and F are ordinary symbols that can be overwritten.
    is_passing <- avg_score > 85 # evaluates to TRUE
  5. 5

    Step 5 — Write the Refactored Script

    The final clean script reads:
    exam_scores <- c(88, 92, 75, 96, 84) avg_score <- mean(exam_scores) is_passing <- avg_score > 85 results_df <- data.frame(scores = exam_scores, pass = is_passing) print(results_df)

The refactored version is not only free of masking issues but is also substantially easier to read. Each variable name tells you what it contains (exam_scores, avg_score, is_passing), and all built-in functions remain accessible at their expected positions on the search path.

SECTION 7

Strengths, Limitations, and Common Objections

Like any convention, strict naming rules involve tradeoffs. The table below evaluates the primary arguments for and against rigorous naming discipline in R, especially in the context of exploratory data analysis where speed of iteration is often prioritized over long-term maintainability.

AspectStrengthLimitation
ReadabilityDescriptive snake_case names are self-documenting, reducing the need for inline comments and lowering onboarding cost for collaborators.Longer names increase line length, which may require more line breaks in pipe-heavy dplyr chains.
Bug PreventionAvoiding masking eliminates an entire category of runtime bugs that are difficult to debug because R provides no warning.In short, interactive sessions, experienced users rely on function-mode lookup to survive incidental masking (e.g., c <- 10 still allows c() calls).
ConsistencyA uniform style across a codebase makes grep/search operations reliable and code review faster.Many popular base-R and Bioconductor packages use camelCase or dot.case internally, creating style clashes when you extend them.
Tooling Supportlintr's object_name_linter and styler enforce conventions automatically in CI/CD pipelines.Configuring linters for mixed-convention projects (e.g., wrapping a camelCase API) requires exception rules.
Typing SpeedModern IDE autocompletion (RStudio, VS Code) means longer names cost negligible extra keystrokes.In math-heavy derivations, terse algebraic names (x, y, n) remain conventional and arguably clearer.

Tradeoff analysis of strict naming conventions in R

✦

KEY TAKEAWAY

Think of naming conventions as the type system R doesn't enforce. In statically typed languages like Java or C++, the compiler prevents you from accidentally treating a number as a function. R's dynamic typing places that responsibility on the programmer. Disciplined naming is the closest thing to a compile-time guarantee that your symbols mean what you intend them to mean.

SECTION 8

Connection to Package Development and Advanced Scoping

The naming principles covered so far apply primarily to scripts and interactive sessions, but they become even more critical—and more nuanced—when developing R packages. In the package ecosystem, NAMESPACE files and explicit imports/exports provide a more formal mechanism for controlling name visibility. Understanding the connection between script-level conventions and package-level namespace management is essential for any R programmer moving from analysis to software engineering.

FeatureScript-Level (This Lesson)Package-Level (Advanced)
Masking PreventionChoose unique names manually; use lintr for enforcementNAMESPACE controls exports; importFrom() selectively imports only needed symbols from dependencies
Naming Conventionsnake_case for all user objectssnake_case plus .onLoad, .onAttach (dot-prefixed hooks); internal helpers prefixed with a dot to hide from users
Scope ControlGlobal environment assignment; function closures for local scopePackage namespace environment (sealed); explicit export list
Collision HandlingUse base::fun() or pkg::fun() to disambiguateroxygen2 @importFrom declarations resolve at build time; R CMD check warns about conflicts

Script-level vs. package-level naming and namespace management

As you advance into R package development, you will encounter additional conventions such as prefixing internal (non-exported) functions with a dot (.compute_internal) to signal that they are not part of the public API, and using roxygen2 tags like @importFrom stats lm to declare precisely which functions your package depends on. These mechanisms formalize the same intuition that drives good naming at the script level: make every symbol's origin and purpose unambiguous.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why assigning T <- 0 at the R console is dangerous, whereas assigning TRUE <- 0 produces an error. What is the fundamental difference between T and TRUE in R's grammar?
Show answer
In R, TRUE and FALSE are reserved words—syntactic constants that the parser treats as literals. Attempting to assign to them produces a parse error: "invalid (do_set) left-hand side to assignment." In contrast, T and F are merely pre-defined global variables bound to TRUE and FALSE respectively. Because they are ordinary symbols, they can be reassigned, which silently changes the meaning of any subsequent code that relies on T or F as Boolean constants. This is why style guides universally recommend spelling out TRUE and FALSE in full.

Question 2: BASIC

Rewrite the following variable names using proper snake_case and avoiding masking. Provide your replacements and briefly justify each. c <- 3.14 list <- c("a", "b", "c") sum <- 100 data.frame.1 <- data.frame(x = 1:5)
Show answer
Suggested replacements: c → pi_approx (descriptive of the value, avoids masking base::c); list → letter_vec (describes content, avoids masking base::list and correctly identifies the type as a character vector); sum → total_amount (avoids masking base::sum); data.frame.1 → sample_df (replaces dots with snake_case, removes the ambiguous numeral suffix, and uses a qualified version of 'df' that doesn't mask stats::df).

Question 3: INTERMEDIATE

A colleague's script fails with the error Error in filter(mtcars, cyl == 4) : object 'cyl' not found. They loaded both library(stats) and library(dplyr) in that order. Use your knowledge of R's search path to diagnose the problem and propose two distinct solutions.
Show answer
Both stats and dplyr export a function named filter. Since dplyr was loaded second, dplyr::filter should be higher on the search path and should work correctly with data-frame columns. However, if the colleague later called library(stats) again, or if another package re-attached stats, then stats::filter (a time-series function expecting different arguments) now masks dplyr's version. Solution 1: Use explicit namespace qualification—dplyr::filter(mtcars, cyl == 4)—which bypasses the search path entirely. Solution 2: Use the conflicted package (library(conflicted); conflict_prefer("filter", "dplyr")), which forces R to throw an error any time an ambiguous call is made, requiring explicit disambiguation.

Question 4: APPLIED

You are writing an R package that processes genomic data. You need a helper function that computes a normalized score. Choose an appropriate function name and two parameter names, following tidyverse conventions. Then write the function signature (no body needed) and explain your naming rationale. Also explain why naming the function scale would be problematic.
Show answer
A well-chosen signature might be: compute_normalized_score <- function(raw_counts, scale_factor = 1e6) { ... }. The function name uses a verb (compute) followed by a descriptive noun phrase (normalized_score), all in snake_case. The parameter raw_counts is a noun describing the input data, and scale_factor uses a qualified noun to distinguish it from the base R function scale(). Naming the function scale would mask base::scale(), which is widely used for centering and scaling matrices. If your package exports scale, any user who loads your package will find that scale() calls your genomic function instead of the standard matrix operation, breaking their existing workflows.

Question 5: CRITICAL THINKING

R's function-mode lookup means that c <- 10; c(1, 2, 3) still returns c(1, 2, 3) correctly. Some argue this makes masking a non-issue for common functions. Construct a concrete, minimal code example where masking c with a non-function value causes actual incorrect results (not just an error), and explain the mechanism.
Show answer
Consider this code: c <- 10 my_func <- function(combine_fn, x, y) combine_fn(x, y) result <- my_func(c, 1:3, 4:6). The programmer intends to pass base::c as a higher-order function argument. However, c in the calling environment resolves to the number 10 via general (non-function-mode) lookup because it appears in argument position, not call position. Inside my_func, combine_fn is bound to 10, and combine_fn(x, y) fails with "could not find function 'combine_fn'." A more insidious variant: if c were assigned a different function (say c <- sum), then my_func(c, 1:3, 4:6) silently returns 21 (the sum) instead of c(1, 2, 3, 4, 5, 6) (the concatenation). This demonstrates that function-mode lookup only protects direct call-position usage; higher-order patterns and metaprogramming are fully vulnerable to masking.

SUMMARY

Summary

Summary

This lesson established that naming conventions in R are far more than cosmetic preferences—they are a critical defense against function masking, a class of bugs arising from R's lexical scoping and linear search path. The recommended approach uses snake_case for all user-defined objects, nouns for data objects and verbs for functions, and avoids identifiers that collide with base-R or commonly loaded package names such as c, mean, data, df, T, and F.

We saw that R's first-match-wins lookup rule means the global environment always takes precedence, and that function-mode lookup provides only partial protection—higher-order function patterns remain fully vulnerable. Tools like lintr, styler, and the conflicted package automate detection and prevention. As you advance into package development, NAMESPACE files and explicit imports formalize these conventions, but the foundation remains the same: choose names that are descriptive, unique, and impossible to confuse with the language's built-in vocabulary.

Varsity Tutors • R Programming • Naming Conventions — Use basic naming conventions and avoid masking functions