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 lineA 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 job