PONYΞ»M2Modula-2
CodeCompared
for R programmers

You already know R.Now explore other languages.

Side-by-side, interactive cheatsheets for R programmers
comparing R to other languages. Every example runs live in your browser β€” no setup, no installation.

β–Ά Start with GoBrowse comparisons ↓Explore the language map β†—

Choose your own path by reordering languages

GoPre-Alpha

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.

  • 🚨 No vectorization AT ALL: 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)
  • Static types everywhere: a variable's type is fixed forever at declaration, implicit coercion is gone, and 1 + true is a compile error instead of a silent 1 + TRUE
  • Errors are ordinary (value, error) return values checked with if err != nil at every call site β€” tryCatch's job spread across the whole function, not wrapped once
  • NA splits into two: a missing map key is the zero value (not NULL) unless you ask with the "comma ok" idiom (value, exists := m[key]) β€” the same shape errors use
  • Real concurrency built into the language: go launches a lightweight goroutine and channels hand off results explicitly, where R's parallel package forks whole OS processes
  • S3 generics become methods with an explicit receiver β€” no inheritance, no dispatch chain, each type simply owns what is declared against its name
  • The payoff: go build produces ONE static binary β€” no R installation, no renv lockfile, no Docker image needed to reproduce the environment on another machine
PythonBeta⚑ Works Offline⚑ Offline

The 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.

  • 🚨 Negative indexing INVERTS: R's x[-1] drops the first element; Python's x[-1] selects the last β€” same syntax, opposite meaning
  • Vectors are not the default: [1, 2, 3] * 2 repeats the list, and elementwise math lives in numpy, where R's instincts finally work again
  • Ranges and slices are half-open β€” 1:5 has five elements, range(1, 5) has four, and 2:4 becomes [1:4]
  • Copy-on-modify is gone: assignment aliases, functions can mutate their arguments, and .copy() is how you buy R's safety back
  • data.frame β†’ pandas: df$age becomes df["age"], aggregate becomes groupby, and pipes become method chains
  • One NA splits into None and nan β€” and pandas aggregations skip missing values by default, the opposite of R
  • The apply family becomes list comprehensions: [dose * 2 for dose in doses if dose > 15] is sapply plus subsetting in one line
C++Pre-Alpha

Rcpp 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.

  • 🚨 R's vectorized operators ALREADY ARE the C++ loop, written once inside R's own C sources β€” so "vectorize it" and "rewrite it in C++" reach the same machine code, and C++ only wins where no vectorized formulation exists
  • Copy-on-modify becomes copies you can see. C++ copies at the assignment every time, and you opt out with & β€” which makes the cost predictable instead of invisible
  • 🚨 Indexing starts at zero, and values[-1] is not "drop the first" β€” it reads memory before the vector, with no error. Negative indexing, logical subsetting and recycling all become explicit loops
  • NA 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 element
  • A data frame becomes a struct OF vectors, not a vector of structs β€” which keeps each column contiguous, exactly as R stores it
  • Rcpp's NumericVector 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()
  • Expect 50-100x on a tight scalar loop and roughly nothing on already-vectorized work. Profile first, move one function, leave the rest in R
FortranPre-Alpha

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.

  • The comfort zone is real: 1-based indexing survives (heights(1) means what it always meant), and whole-array arithmetic (doses * 2.0, sqrt(doses)) vectorizes with no loop, exactly like R
  • 🚨 Recycling becomes a hard error: mismatched array shapes must conform EXACTLY β€” R's silent (or warned) shorter-vector repeating has no equivalent, catching a whole class of silent bugs before the program runs
  • Every variable has one fixed, DECLARED type (implicit none makes an undeclared name a compile error) β€” no more discovering a type mismatch mid-analysis
  • There is no NA at all: missingness needs an agreed sentinel value (often -999) checked by hand everywhere β€” the discipline na.rm = TRUE automates away in R
  • R's free vectorization becomes explicit: mark a function elemental to let it accept a scalar OR any array shape β€” requested, not assumed
  • Named lists become declared derived types (type :: patient_type, accessed with %) β€” fields fixed and typed, so a misspelled field is a compile error, not a silent NULL
  • The honest payoff: R already depends on Fortran one layer down β€” LAPACK/BLAS underneath R's own matrix operations are Fortran, and it is the well-worn path for rewriting a slow inner loop once profiling finds R itself is the bottleneck
ScalaPre-Alpha

Where 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).

  • dplyr verbs map straight onto collection methods β€” 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)
  • Nothing vectorizes: doses * 2 does not compile β€” map(_ * 2) is the universal spelling, and there is no built-in mean
  • No NA: absence lives in the type as Option β€” Seq[Option[Double]] will not sum until you flatten (na.rm) or getOrElse (replace), decided at compile time
  • Copy-on-modify formalized: default collections are immutable, every "modification" returns a new one via structural sharing β€” the R safety guarantee, by construction
  • Named lists grow into case classes: typed fields, .copy(age = 37) for non-destructive update, and a misspelled field is a compile error instead of a silent NULL
  • switch grows into match: destructuring patterns over sealed hierarchies, with the compiler warning when a case is missing β€” S3 dispatch with coverage checking
  • Comforts that survive: if as an expression, default + named arguments, inclusive 1 to 5 ranges, and the last expression as the return value
JuliaPre-Alpha

The 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.

  • The comfort zone is real: 1-based indexing, inclusive 1:5 ranges, logical-mask subsetting, functions (not methods), and the last expression is still the return value
  • Vectorization becomes EXPLICIT: sqrt.(values), doses .* 2 β€” the dot broadcasts ANY function elementwise, replacing R's invisible (and sometimes absent) vectorization
  • 🚨 x[-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 silently
  • Copy-on-modify is gone, but every mutating function says so in its name: sort copies like R's, sort! mutates β€” the ! convention is the language-wide warning label
  • missing propagates with NA's exact three-valued logic, skipmissing plays na.rm = TRUE, and nothing keeps NULL's separate job
  • Loops are fast (JIT-compiled to machine code), so the two-language problem β€” prototype in R, rewrite in C++ β€” disappears
NushellPre-Alpha

Your 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 got
  • from json, from csv, from yaml and from toml are builtins, so no jsonlite to install and no package convention per format
  • Arithmetic is not vectorized and there is no recycling β€” $numbers * 2 is an error, and elementwise work is an explicit each
  • Indexing is 0-based, and a negative index does not exist at all rather than meaning "drop" β€” skip and drop say which end and how many
  • null is absence, not NA: math sum skips it silently instead of propagating, so missingness is yours to count
  • A table is row-major, so apply(df, 1, …) stops being the trap where a data frame is coerced to a matrix and every number becomes a string
Drag cards to reorder Β· your order is saved locally