An infix function in R is a function you call between its two arguments, like x + y or a %in% b, instead of the usual func(x, y) prefix style. You can define your own infix operators using the %name% syntax, turning any two-argument function into a clean, readable operator.
What Are Infix Functions and Why Does R Use Them?
Every operator you type in R, +, -, *, <, even [, is secretly a regular function. R just lets you write it between the arguments instead of wrapping them in parentheses. This "infix" placement is why 3 + 5 feels natural while ` +(3, 5) ` feels clunky. Let's prove it:
Both forms produce identical results because the infix style is syntactic sugar. When R sees 3 + 5, it internally translates that to ` +(3, 5) . The same applies to every operator — arithmetic (+, -, *, /), comparison (<, ==, >=), logical (&, |), and special operators like %in%`.
+ in 3 + 5 is syntactic sugar for calling the function +() with two arguments. This means you can pass operators to higher-order functions like Reduce(), store them in variables, and, most importantly, create your own.This insight unlocks a powerful idea: if built-in operators are just two-argument functions with special names, nothing stops you from making your own. R reserves the %name% syntax specifically for user-defined infix operators.
Try it: Convert the expression x * y - z into fully prefix form using backtick notation. Assign x <- 4, y <- 3, z <- 2 and verify both forms give the same result.
Click to reveal solution
Explanation: x * y becomes ` *(x, y) , and the outer - z wraps that result: -(*(x, y), z) `. Read it inside-out, multiply first, then subtract.
How Do You Create a Custom %op% Infix Operator?
Creating your own infix operator takes three steps: pick a name wrapped in %, define a function with exactly two arguments, and assign it using backticks around the %name%. Let's start with the simplest possible example:
That's it, %plus% is now a working infix operator. The backticks in the definition tell R that %plus% is the function name (special characters need backtick-quoting). When calling the operator, no backticks are needed, just write a %plus% b.
Let's build something more useful. A common need is checking divisibility:
Notice that %divisible% is automatically vectorized because %% and == are vectorized. Your operator inherits whatever behavior its body uses.
Here's one more, a string concatenation operator that replaces nested paste0() calls:
The infix version reads left to right, just like the sentence it builds. The nested version forces you to read inside-out.
%ni% (not in) beat %not_in_vector%. Your future self should be able to guess what x %op% y does from the name alone, without checking the definition.Try it: Create a %between% operator that checks if x falls between the minimum and maximum of a length-2 vector y (inclusive). Test with 15 %between% c(10, 20).
Click to reveal solution
Explanation: min(y) and max(y) extract the bounds, then >= and <= with & check that x falls within the range. Vectorization comes free from the comparison operators.
What Naming Rules Apply to Custom Infix Operators?
R is surprisingly permissive with infix operator names. The only hard rule is: no % character inside the name. Everything else, spaces, punctuation, Unicode, is technically allowed.
Both work because R allows any sequence of characters between the % delimiters. You escape special characters in the definition (with backticks), but callers write the operator naturally.
The one thing you absolutely cannot do is put a % inside the name:
R's parser sees the second % as the closing delimiter and gets confused by what follows.
%ni%, %like%, %has%. Exotic names like %=>% or spaces in names cause headaches in team codebases and can break syntax highlighting in some editors.Try it: Create an operator called %greet% that pastes the left argument, " says hello to ", and the right argument. Test with "Alice" %greet% "Bob".
Click to reveal solution
Explanation: paste() joins strings with spaces by default. The operator wraps a common string-building pattern into a readable infix call.
How Does Operator Precedence Work With Custom Infix Operators?
All custom %op% operators share the same precedence level, they sit between addition/subtraction and comparison operators in R's precedence table. They also associate left to right. Both facts can surprise you.
Let's see left-to-right association in action:
The output shows that "a" %w% "b" evaluates first, then that result feeds into %w% "c". Left to right, just like 3 - 2 - 1 evaluates as (3 - 2) - 1.
Now here's the precedence surprise. Custom %op% operators bind tighter than + and -:
Because %plus% has higher precedence than -, R evaluates 2 %plus% 3 first, then subtracts that result from 10. But * has higher precedence than %plus%, so 2 * 3 evaluates before the infix operator gets involved.
1 + x %op% y means 1 + (x %op% y), not (1 + x) %op% y. When mixing custom operators with arithmetic, add parentheses to make your intent explicit.Try it: Given the %plus% operator already defined, predict what 2 + 3 %plus% 4 returns. Then predict what 2 - 3 %plus% 4 returns. Run both to check.
Click to reveal solution
Explanation: In both cases, %plus% evaluates before + or - because it has higher precedence. The first example 2 + 7 = 9 looks unsurprising, but the second example 2 - 7 = -5 reveals the trap, many people expect left-to-right reading and would predict (2 - 3) + 4 = 3.
What Are Some Practical Custom Infix Operators You Can Build?
The toy examples above teach the syntax. Now let's build operators you'll actually use. Each one replaces a pattern you'd otherwise write over and over.
The "not in" operator is the most-requested missing operator in base R. Every R programmer has written !x %in% y and wondered why there's no built-in negation:
!x %in% y ten times a day, %ni% saves keystrokes and prevents a subtle precedence bug: !x %in% y actually means (!x) %in% y for non-logical x, the ! binds to x first, not the whole expression. With %ni%, the negation is safely inside the function.Null coalescing gives a default value when the left side is NULL, like Python's or or JavaScript's ??:
This is invaluable when working with function arguments or list elements that might be NULL.
Case-insensitive pattern matching wraps grepl() into a readable operator:
And string concatenation without nested paste0():
Try it: Create a %clamp% operator that clamps x to the range defined by a length-2 vector y. Values below min(y) become min(y), values above max(y) become max(y). Test with 150 %clamp% c(0, 100).
Click to reveal solution
Explanation: pmax(x, min(y)) raises any value below the minimum, and pmin(..., max(y)) caps any value above the maximum. Using pmin/pmax instead of min/max keeps the operation vectorized.
When Should You Write a Custom Infix Operator (and When Should You Not)?
Custom operators are powerful, but the power comes with a responsibility: every new operator is a symbol your collaborators need to learn. Here's how to decide.
Let's compare the same logic written with and without custom operators:
The custom operator version reads more like English. But notice, the standard version is also perfectly clear. The operators help most when the pattern repeats dozens of times across a project.
x %op% y does from the name alone, use a regular function instead. A good test: would you put it in your project's utility file and expect everyone on the team to adopt it?Guidelines for good judgment:
- Do create operators for patterns you use 10+ times per project, team-agreed conventions, and package APIs where readability matters (like pipe variants).
- Don't create operators for one-off transformations, complex multi-step logic (more than 5 lines in the body), or names that require explanation.
- Package operators like
%>%,%in%, and%<>%work because they're documented, widely known, and part of an ecosystem. Your custom%magic%operator in a solo script doesn't have those advantages.
Try it: Write a %titlecase% operator that capitalizes the first letter of a string and leaves the rest unchanged. Test with "hello" %titlecase% NULL. Then think about whether this should be an infix operator or a regular function.
Click to reveal solution
Explanation: The infix version wastes its right argument, a sign this operation isn't naturally binary. titlecase("hello") is cleaner than "hello" %titlecase% NULL. When you find yourself ignoring an argument, that's a strong hint to use a regular function instead.
Practice Exercises
Exercise 1: Create a %swap% Operator
Write a %swap% operator that takes a vector on the left and a length-2 integer vector on the right (two positions), then returns the vector with those positions swapped. For example, c("a", "b", "c", "d") %swap% c(2, 4) should return c("a", "d", "c", "b").
Click to reveal solution
Explanation: We copy the vector first to avoid overwriting a value before it's been read. Then we assign each position's value from the original vector's opposite position.
Exercise 2: Create a %chain% Function Composition Operator
Create a %chain% operator that composes two functions left-to-right: (f %chain% g)(x) should equal g(f(x)). This is the opposite of mathematical composition (which is right-to-left). Test with (sqrt %chain% round)(10), it should compute round(sqrt(10)) which is 3.
Click to reveal solution
Explanation: %chain% returns a new function (a closure) that applies f first, then passes the result to g. The ... lets the composed function accept any arguments the first function needs. Left-to-right chaining works because %chain% associates left to right: f %chain% g %chain% h becomes (f %chain% g) %chain% h, which applies f, then g, then h.
Exercise 3: Create a %where% Data Frame Filter Operator
Create a %where% operator that filters a data frame (left side) using a quoted condition string (right side). For example, mtcars %where% "mpg > 25 & cyl == 4" should return only the matching rows. Use parse() and eval() to evaluate the condition string within the data frame's context.
Click to reveal solution
Explanation: parse(text = condition) converts the string into an R expression, and eval(expr, envir = df) evaluates it with the data frame's columns as variables. This produces a logical vector that subsets the rows. Note: eval(parse(...)) on user-supplied strings is a security risk in production, fine for interactive analysis, but don't accept untrusted input.
Putting It All Together
Let's use our operator toolkit in a realistic data-analysis workflow. We already defined %ni%, %??%, %+%, and %between% earlier in this session, so they're available in our WebR environment. Here's a complete example that chains them together:
Each operator replaces a common pattern: %between% replaces a double comparison, %ni% replaces !... %in% ... (avoiding the precedence trap), %+% replaces nested paste0(), and %??% provides a clean default. Together, they make the analysis code read almost like pseudocode.
Summary
| Concept | Key Point | Example |
|---|---|---|
| What is an infix function? | A function called between its two arguments | 3 + 5 calls ` +(3, 5) ` |
| How to create one | Name with %, define with backticks, 2 arguments |
` %ni% <- function(x, y) !(x %in% y) ` |
| Naming rules | Any characters except % inside the name |
%like%, %=>% are valid; %50%% is not |
| Precedence | All %op% share one level: tighter than +, looser than * |
1 + x %op% y means 1 + (x %op% y) |
| Practical operators | Encode repeated patterns as operators | %ni%, %??%, %like%, %+%, %between% |
| When to use | When calling code becomes clearer for the whole team | Use for 10+ repetitions; skip for one-off logic |
| When NOT to use | When the name needs explanation or the body is complex | Prefer regular functions for multi-step logic |
References
- Wickham, H., Advanced R, 2nd Edition. Chapter 6: Functions, §6.8 Function Forms. Link
- R Core Team, R Language Definition, §3.1.4 Operators. Link
- R Core Team, An Introduction to R, §10.2 Defining New Binary Operators. Link
- R Documentation, Syntax: Operator Precedence Table. Link
- Wickham, H. & Grolemund, G., R for Data Science, 2nd Edition. Link
- magrittr package documentation, Pipe operators. Link
Continue Learning
- R Function Operators, Learn
compose(),negate(),partial(), andmemoise()to transform existing functions without rewriting them. - Functional Programming in R, The complete guide to first-class functions, closures, and functional patterns in R.
- R Currying and Partial Application, Pre-fill function arguments for cleaner, more reusable pipelines.