Side-by-side, interactive cheatsheets for R programmers
comparing R to other languages. Every example runs live in your browser β no setup, no installation.
Choose your own path by reordering languages
What R structurally cannot offer: a single static binary, real concurrency, and a compiler that catches mistakes before a long job discovers them at 2am. The landing is a deliberate culture shock β no REPL-first workflow, no vectorization at all (not even a library restores it), static types everywhere, and NA's whole job reassigned to explicit (value, bool) pairs β traded for goroutines, channels, and `go build` producing one file that just runs, anywhere.
doses * 2 does not compile, and no library restores it β a for/range loop is the ONLY way to touch every element (loops are fast here, unlike R's)1 + true is a compile error instead of a silent 1 + TRUE(value, error) return values checked with if err != nil at every call site β tryCatch's job spread across the whole function, not wrapped oncevalue, exists := m[key]) β the same shape errors usego launches a lightweight goroutine and channels hand off results explicitly, where R's parallel package forks whole OS processesgo build produces ONE static binary β no R installation, no renv lockfile, no Docker image needed to reproduce the environment on another machineThe other data-science language β similar on the surface, inverted underneath. Python looks like R with cleaner syntax, but the first week is a minefield: indexing starts at zero, x[-1] selects instead of drops, [1, 2, 3] * 2 repeats the list, and your data is no longer copied on assignment. The reward is the production ecosystem R never grew.
x[-1] drops the first element; Python's x[-1] selects the last β same syntax, opposite meaning[1, 2, 3] * 2 repeats the list, and elementwise math lives in numpy, where R's instincts finally work again1:5 has five elements, range(1, 5) has four, and 2:4 becomes [1:4].copy() is how you buy R's safety backdata.frame β pandas: df$age becomes df["age"], aggregate becomes groupby, and pipes become method chainsNA splits into None and nan β and pandas aggregations skip missing values by default, the opposite of Rapply family becomes list comprehensions: [dose * 2 for dose in doses if dose > 15] is sapply plus subsetting in one lineRcpp is why this page exists. It is on well over two thousand CRAN packages and is how every serious R package gets fast β so you are not changing careers, you have a loop that will not vectorize or a package whose C++ you inherited. Two R facts do the most work here: everything is a vector, and copy-on-modify. Neither survives, and both were load-bearing.
& β which makes the cost predictable instead of invisiblevalues[-1] is not "drop the first" β it reads memory before the vector, with no error. Negative indexing, logical subsetting and recycling all become explicit loopsNA has no equivalent, which needs a design decision rather than a translation: NaN works for doubles only, and std::optional costs a byte and a branch per elementNumericVector IS R's vector, so a million elements cross for free β but it is zero-indexed, and modifying it modifies the caller's data, breaking copy-on-modify unless you clone()A rare, genuine kinship: the two languages most native to arrays. Fortran, like R, vectorizes whole-array arithmetic with no explicit loop, and BOTH count from one β the single most disorienting habit most migrations force never happens here. What changes is the substrate: strict shape conformance instead of silent recycling, static declared types, a compile step, and NA replaced by a hand-checked sentinel value.
heights(1) means what it always meant), and whole-array arithmetic (doses * 2.0, sqrt(doses)) vectorizes with no loop, exactly like Rimplicit none makes an undeclared name a compile error) β no more discovering a type mismatch mid-analysis-999) checked by hand everywhere β the discipline na.rm = TRUE automates away in Relemental to let it accept a scalar OR any array shape β requested, not assumedtype :: patient_type, accessed with %) β fields fixed and typed, so a misspelled field is a compile error, not a silent NULLWhere sparklyr's training wheels come off. Spark is written in Scala, and the native API is what your dplyr verbs were being translated into all along. The shape of the work survives β chains that read like pipelines, if-as-expression, named arguments, data that never mutates underneath you β while the substrate changes to static types, zero-based parentheses, and missingness moved out of the data (NA) into the type system (Option).
filter/sortBy/map/groupBy chain with dots, reading exactly like a magrittr pipeline (no pipe operator needed)x[1] becomes x(0) β zero-based AND parenthesized; negative-index dropping becomes named methods (tail, init, drop)doses * 2 does not compile β map(_ * 2) is the universal spelling, and there is no built-in meanOption β Seq[Option[Double]] will not sum until you flatten (na.rm) or getOrElse (replace), decided at compile time.copy(age = 37) for non-destructive update, and a misspelled field is a compile error instead of a silent NULLswitch grows into match: destructuring patterns over sealed hierarchies, with the compiler warning when a case is missing β S3 dispatch with coverage checkingif as an expression, default + named arguments, inclusive 1 to 5 ranges, and the last expression as the return valueThe promise on the label: R's interactive, vector-first feel WITH compiled speed. Indexing is still 1-based, ranges are still inclusive, missing is a real NA with three-valued logic, and S3-style generic functions grow up into multiple dispatch β while the JIT makes plain loops as fast as C, ending the vectorize-or-suffer culture for good.
1:5 ranges, logical-mask subsetting, functions (not methods), and the last expression is still the return valuesqrt.(values), doses .* 2 β the dot broadcasts ANY function elementwise, replacing R's invisible (and sometimes absent) vectorizationx[-1] is a BoundsError, not a drop β exclusion is spelled x[2:end], and recycling is gone entirely (mismatched lengths error instead of silently wrapping)* and %*% swap jobs on matrices: bare * IS matrix multiplication, elementwise needs .* β port a formula untranslated and it computes the wrong thing silentlysort copies like R's, sort! mutates β the ! convention is the language-wide warning labelmissing propagates with NA's exact three-valued logic, skipmissing plays na.rm = TRUE, and nothing keeps NULL's separate jobYour pipeline, reaching outside the process. You already have a structured pipeline β dplyr's filter/select/mutate/group_by is Nushell's where/select/insert/group-by, near enough word for word. What Nushell adds is that the same pipeline runs over files, directories, JSON APIs and other programs' output, where R has to go through readLines, a package, or a system() call that hands back text to parse.
ls returns a table with size, type and modified already in it β no second trip to file.info() with the names you just gotfrom json, from csv, from yaml and from toml are builtins, so no jsonlite to install and no package convention per format$numbers * 2 is an error, and elementwise work is an explicit eachskip and drop say which end and how manynull is absence, not NA: math sum skips it silently instead of propagating, so missingness is yours to countapply(df, 1, β¦) stops being the trap where a data frame is coerced to a matrix and every number becomes a string