PONY λ M2 Modula-2

R.CodeCompared.To/Scala

An interactive executable cheatsheet comparing R and Scala

R 4.6 Scala 2.13
Basics
Hello, World
cat("Hello, World!\n")
object Main { def main(args: Array[String]): Unit = { println("Hello, World!") } }
The first difference is the ceremony: Scala is compiled, and a program needs an entry point — an object Main with a main method — before it prints anything. Every example on this page carries this wrapper. println adds the newline cat makes you write.
<- becomes val (and bindings stop moving)
sample_size <- 30 sample_size <- 31 # any binding can be freely reassigned mean_height <- 170.5 cat(sample_size, mean_height, "\n")
object Main { def main(args: Array[String]): Unit = { val sampleSize = 30 // val: cannot be reassigned — the default var meanHeight = 170.0 // var: reassignable, used sparingly meanHeight = 171.5 println(sampleSize + " " + meanHeight) } }
Assignment splits in two: val declares a name that can never be rebound (the overwhelming default in idiomatic Scala), var one that can. The types are there — sampleSize is an Int, meanHeight a Double — but inferred, so declarations read as lightly as R’s. Convention also switches from snake_case to camelCase.
if is still an expression
sample_size <- 12 label <- if (sample_size >= 30) "large" else "small" print(label) # and ifelse() handles the vectorized case print(ifelse(c(10, 40) >= 30, "large", "small"))
object Main { def main(args: Array[String]): Unit = { val sampleSize = 12 val label = if (sampleSize >= 30) "large" else "small" println(label) } }
A genuine comfort: like R (and unlike Python or Java), Scala’s if is an expression with a value, so the assign-a-conditional idiom transfers verbatim. There is no vectorized ifelse — that becomes values.map(value => if (...) ... else ...), the shape the collections section explains.
Zero-Based, in Parentheses
🚨 x[1] becomes x(0)
heights <- c(160, 172, 181) print(heights[1]) # the first element print(heights[length(heights)]) # the last
object Main { def main(args: Array[String]): Unit = { val heights = Vector(160, 172, 181) println(heights(0)) // parentheses — and counting starts at zero println(heights.head) // or ask by name println(heights.last) // no length arithmetic needed } }
Two changes stack: counting starts at zero, and indexing uses parentheses, not brackets — heights(0) is really the method call heights.apply(0). In practice idiomatic Scala indexes rarely: head, last, and the collection methods in the next sections ask for elements by role instead of by position.
x[-1] → tail (dropping by name)
heights <- c(160, 172, 181, 195) print(heights[-1]) # drop the first print(heights[-length(heights)]) # drop the last print(heights[-c(1, 2)]) # drop the first two print(tail(heights, 2)) # keep the last two
object Main { def main(args: Array[String]): Unit = { val heights = Vector(160, 172, 181, 195) println(heights.tail) // drop the first — R's heights[-1] println(heights.init) // drop the last println(heights.drop(2)) // drop the first two println(heights.takeRight(2)) // keep the last two } }
Negative indexing does not exist; every exclusion R spells with a minus sign has a named method — tail, init, drop, dropRight, take, takeRight. The names read better than the arithmetic, and there is no silent behavior change waiting when a negative number arrives at an index by accident.
Collections Without Vectorization
map is the universal spelling
doses <- c(1, 2, 3) print(doses * 2) # arithmetic vectorizes invisibly print(sqrt(doses)) print(mean(doses))
object Main { def main(args: Array[String]): Unit = { val doses = Vector(1.0, 2.0, 3.0) println(doses.map(_ * 2)) // nothing vectorizes on its own println(doses.map(math.sqrt)) println(doses.sum / doses.length) // mean, assembled by hand } }
There is no elementwise arithmetic on collections — doses * 2 does not compile. map is the one universal spelling for “apply this to each element,” with the underscore as shorthand for a one-argument anonymous function. Note also what base Scala lacks: no built-in mean or sd — summary statistics come from libraries (Breeze, Spark) or three characters of arithmetic.
1:5 → 1 to 5 (still inclusive)
print(1:5) # inclusive print(seq(0, 10, by = 2)) for (trial in 1:3) { cat("trial", trial, "\n") }
object Main { def main(args: Array[String]): Unit = { println((1 to 5).toList) // inclusive, like R's 1:5 println((0 to 10 by 2).toList) // seq(by =) reads as English println((1 until 5).toList) // half-open, when you want it for (trial <- 1 to 3) println("trial " + trial) } }
Ranges keep R’s inclusive ends — 1 to 5 has five elements — and seq(0, 10, by = 2) becomes the almost-English 0 to 10 by 2. The until variant is half-open for the occasions zero-based indexing wants it. These read as method calls because they are: to, by, and until are ordinary methods on Int.
sapply + subset → for/yield
doses <- c(10, 20, 30) print(sapply(doses, \(dose) dose / 10)) print(doses[doses > 15]) print(sapply(doses[doses > 15], \(dose) dose / 10))
object Main { def main(args: Array[String]): Unit = { val doses = Vector(10, 20, 30) val scaled = for (dose <- doses if dose > 15) yield dose / 10.0 println(scaled) } }
The for/yield comprehension covers sapply plus logical subsetting in one construct: iterate, filter with the inline if, and yield builds a new collection of the results. It is syntactic sugar for the filter/map chains of the next section — use whichever reads better case by case.
dplyr Verbs → Collection Methods
The pipeline reads the same
survey <- data.frame( name = c("Ada", "Grace", "Mary"), age = c(36, 41, 58) ) adults <- subset(survey, age > 40) adults <- adults[order(adults$age, decreasing = TRUE), ] print(adults$name)
case class Person(name: String, age: Int) object Main { def main(args: Array[String]): Unit = { val survey = Seq(Person("Ada", 36), Person("Grace", 41), Person("Mary", 58)) val names = survey .filter(_.age > 40) .sortBy(person => -person.age) .map(_.name) println(names) } }
The dplyr vocabulary maps almost one-to-one onto collection methods: filter is filter, arrange is sortBy, mutate/select are map, distinct is distinct — and the chain reads exactly like a magrittr pipeline with dots for pipes. The “data frame” here is a plain Seq of case-class rows; Spark’s DataFrame API (the ecosystem section) scales this same shape to a cluster.
aggregate → groupBy
measurements <- data.frame( group = c("a", "a", "b", "b"), value = c(10, 20, 30, 40) ) print(aggregate(value ~ group, data = measurements, FUN = mean))
case class Measurement(group: String, value: Double) object Main { def main(args: Array[String]): Unit = { val measurements = Seq( Measurement("a", 10.0), Measurement("a", 20.0), Measurement("b", 30.0), Measurement("b", 40.0) ) val averages = measurements .groupBy(_.group) .map { case (group, rows) => group -> rows.map(_.value).sum / rows.length } println(averages.toSeq.sortBy(_._1)) } }
Split-apply-combine is groupBy followed by a map over the resulting (key, rows) pairs — more manual than a formula, but the same shape Spark’s groupBy(...).agg(...) industrializes. One habit to note: groupBy returns an unordered Map, so deterministic output means sorting at the end.
Immutability by Default
Copy-on-modify, formalized
original <- c(1, 2, 3) appended <- c(original, 4) updated <- original updated[1] <- 99 print(original) # always untouched — R copies on modify print(appended) print(updated)
object Main { def main(args: Array[String]): Unit = { val original = Vector(1, 2, 3) val appended = original :+ 4 // a NEW vector — original untouched val updated = original.updated(0, 99) // likewise println(original) println(appended) println(updated) } }
The R instinct that “my data cannot change out from under me” — the one Python and Julia break — is a design principle here: the default collections are immutable, and every “modification” (:+, updated, filter) returns a new collection. Structural sharing makes it cheap, unlike R’s full copies, and scala.collection.mutable is the explicit opt-out for hot loops.
Functions
Functions grow type annotations
standardize <- function(values) { (values - mean(values)) / sd(values) # last expression is returned } print(standardize(c(10, 20, 30)))
object Main { def standardize(values: Seq[Double]): Seq[Double] = { val average = values.sum / values.length val variance = values.map(value => math.pow(value - average, 2)).sum / (values.length - 1) val deviation = math.sqrt(variance) values.map(value => (value - average) / deviation) // still returned as the last expression } def main(args: Array[String]): Unit = { println(standardize(Seq(10.0, 20.0, 30.0))) } }
Parameter types are mandatory (values: Seq[Double]) and the return type is good manners; in exchange, calling standardize("oops") is a compile error instead of a runtime surprise mid-analysis. One comfort survives intact: the last expression is the return value, no return keyword needed.
\(x) → _ (the underscore shorthand)
doses <- c(1, 2, 3) print(sapply(doses, \(dose) dose * 2)) print(Filter(\(dose) dose > 1, doses))
object Main { def main(args: Array[String]): Unit = { val doses = Vector(1, 2, 3) println(doses.map(dose => dose * 2)) // full form println(doses.map(_ * 2)) // underscore shorthand println(doses.filter(_ > 1).map(_ + 10)) } }
The full anonymous-function form is argument => expression; when the argument is used exactly once, the underscore stands in for it — _ * 2 is Scala’s tersest lambda. Each underscore in an expression is a different argument (_ + _ takes two), which surprises everyone once.
Named arguments and defaults survive
rescale <- function(values, center = 0, scale = 1) { (values - center) / scale } print(rescale(c(10, 20, 30), center = 20)) print(rescale(center = 20, c(10, 20, 30)))
object Main { def rescale(values: Seq[Double], center: Double = 0.0, scale: Double = 1.0): Seq[Double] = values.map(value => (value - center) / scale) def main(args: Array[String]): Unit = { println(rescale(Seq(10.0, 20.0, 30.0), center = 20.0)) println(rescale(center = 20.0, values = Seq(10.0, 20.0, 30.0))) } }
Two R conveniences most compiled languages drop both survive: default argument values in the signature, and calling any argument by name — in any order, once named. The one difference is that R’s partial name matching (cent = 20) does not exist; names must be exact, and the compiler tells you when they are not.
... → Double* (slurp and splat)
report <- function(label, ...) { extras <- list(...) cat(label, ":", length(extras), "extra values\n") } report("run", 1, 2, 3) do.call(report, c(list("splat"), as.list(4:6)))
object Main { def report(label: String, values: Double*): Unit = println(label + ": " + values.length + " extra values") def main(args: Array[String]): Unit = { report("run", 1, 2, 3) val numbers = Seq(4.0, 5.0, 6.0) report("splat", numbers: _*) // splat a collection into arguments } }
R’s ... becomes a typed variadic parameter — values: Double* arrives as a ready-to-use sequence, no list(...) unwrapping — and the : _* ascription splats an existing collection into the argument list, retiring do.call. Being typed, the slurp only accepts what it declares: a stray string in the arguments is a compile error.
NA → Option
Absence moves into the type
readings <- list(monday = 3.1, tuesday = 2.7) print(readings$monday) print(readings$sunday) # NULL — silently value <- if (is.null(readings$sunday)) 0 else readings$sunday print(value)
object Main { def main(args: Array[String]): Unit = { val readings = Map("monday" -> 3.1, "tuesday" -> 2.7) println(readings.get("monday")) // Some(3.1) println(readings.get("sunday")) // None — absence as a VALUE println(readings.get("monday").getOrElse(0.0)) println(readings.get("sunday").getOrElse(0.0)) } }
Scala has no NA and (in idiomatic code) no NULL-alike: a lookup that can fail returns Option[Double] — either Some(value) or None — and the type forces the “what if it is missing?” decision at compile time, where R lets NULL slide silently until something downstream misbehaves. getOrElse is the one-line fallback.
na.rm → flatten
values <- c(1, 2, NA, 4) print(mean(values)) # NA propagates print(mean(values, na.rm = TRUE)) values[is.na(values)] <- 0 print(values)
object Main { def main(args: Array[String]): Unit = { val values = Seq(Some(1.0), Some(2.0), None, Some(4.0)) val present = values.flatten // drops the Nones — na.rm = TRUE println(present) println(present.sum / present.length) println(values.map(_.getOrElse(0.0))) // or replace them } }
A column with missing values is Seq[Option[Double]] — the missingness is in the type, not hidden in the data — and arithmetic will not compile until it is dealt with: flatten drops the Nones (the na.rm = TRUE move), map(_.getOrElse(...)) replaces them. There is no propagating NA; the compiler makes you choose up front, once. (Spark’s columns handle nulls more R-like, with na.drop/na.fill.)
Named Lists → Case Classes
list(name =, age =) → case class
patient <- list(name = "Ada", age = 36) class(patient) <- "patient" print(patient$name) str(patient) print(identical(patient, structure(list(name = "Ada", age = 36), class = "patient")))
case class Patient(name: String, age: Int) object Main { def main(args: Array[String]): Unit = { val ada = Patient("Ada", 36) println(ada.name) println(ada) // readable printing for free println(ada == Patient("Ada", 36)) // structural equality for free } }
R’s tagged named list becomes a case class: fields declared and typed once, accessed with . instead of $ — and misspelling a field is a compile error, where patient$aeg silently returns NULL. The case keyword bundles what R makes you improvise: readable printing, structural equality, and the destructuring the pattern-matching section uses.
Non-destructive update via copy
patient <- list(name = "Ada", age = 36) updated <- patient updated$age <- 37 # copy-on-modify: the original never changes print(patient$age) print(updated$age)
case class Patient(name: String, age: Int) object Main { def main(args: Array[String]): Unit = { val ada = Patient("Ada", 36) val older = ada.copy(age = 37) // a new value — ada is untouched println(ada.age) println(older.age) } }
What R does implicitly through copy-on-modify, Scala does explicitly through copy: name the fields that change, keep the rest, get a new value. Case-class instances are immutable, so the original genuinely cannot drift — the same safety R gives, delivered by construction rather than by copying.
switch → match
switch → match
describe <- function(status) { switch(status, pending = "still running", complete = "all done", paste("unknown:", status) # unnamed = the default ) } cat(describe("pending"), "\n") cat(describe("failed"), "\n")
object Main { def describe(status: String): String = status match { case "pending" => "still running" case "complete" => "all done" case other => "unknown: " + other } def main(args: Array[String]): Unit = { println(describe("pending")) println(describe("failed")) } }
match is an expression (like everything here), tries its cases top to bottom, and binds the fallback to a name instead of R’s anonymous trailing argument. This literal-matching form is the shallow end; the next row is why match is one of Scala’s defining features.
Destructuring, checked for exhaustiveness
# S3 dispatch: each class needs a method, and nothing checks coverage predict_response <- function(model, input) UseMethod("predict_response") predict_response.linear <- function(model, input) { model$slope * input + model$intercept } predict_response.constant <- function(model, input) model$value linear <- structure(list(slope = 2, intercept = 1), class = "linear") constant <- structure(list(value = 5), class = "constant") print(predict_response(linear, 10)) print(predict_response(constant, 10))
sealed trait Model case class Linear(slope: Double, intercept: Double) extends Model case class Constant(value: Double) extends Model object Main { def predict(model: Model, input: Double): Double = model match { case Linear(slope, intercept) => slope * input + intercept case Constant(value) => value } def main(args: Array[String]): Unit = { println(predict(Linear(2.0, 1.0), 10.0)) println(predict(Constant(5.0), 10.0)) } }
The S3 pattern — a family of classes, behavior chosen by class — becomes a sealed trait with case-class variants, and match destructures each variant’s fields right in the pattern. sealed is the payoff: the compiler knows every possible Model and warns when a case is missing, where S3 offers only the hope that every class got a method.
Strings
paste & sprintf → s"..." and f"..."
name <- "Ada" trials <- 3 completion <- 66.67 cat(paste("Subject", name, "completed", trials, "trials"), "\n") cat(sprintf("%s: %.1f%% done\n", name, completion))
object Main { def main(args: Array[String]): Unit = { val name = "Ada" val trials = 3 val completion = 66.67 println(s"Subject $name completed $trials trials") println(f"$name%s: $completion%.1f%% done") } }
The s interpolator splices values inline, retiring most paste calls; the f interpolator is sprintf with the format spec attached directly to each value — and, unlike sprintf, the compiler checks that %.1f is actually applied to a number. Joining a vector inverts subject and verb: paste(words, collapse = ", ") is words.mkString(", ").
String functions → methods
phrase <- "stitch in time" print(toupper(phrase)) print(nchar(phrase)) print(strsplit(phrase, " ")[[1]]) print(sub("time", "space", phrase)) print(grepl("time", phrase))
object Main { def main(args: Array[String]): Unit = { val phrase = "stitch in time" println(phrase.toUpperCase) println(phrase.length) println(phrase.split(" ").toList) println(phrase.replace("time", "space")) println(phrase.contains("time")) } }
Function-wrapping becomes method-calling — toupper(x) is x.toUpperCase — and the strsplit(...)[[1]] dance disappears because split returns the array directly. replace here is literal; regular expressions use replaceAll or Scala’s Regex class with pattern matching.
Error Handling
tryCatch → Try (a value, like yours)
result <- tryCatch({ stop("model failed to converge") }, error = function(condition) { cat("caught:", conditionMessage(condition), "\n") NA }) print(result)
import scala.util.{Try, Success, Failure} object Main { def main(args: Array[String]): Unit = { val parsed = Try("3.5".toDouble) val broken = Try("not a number".toDouble) println(parsed.getOrElse(Double.NaN)) println(broken.getOrElse(Double.NaN)) broken match { case Success(value) => println("parsed: " + value) case Failure(caught) => println("caught: " + caught.getMessage) } } }
R users already think of error handling as producing a valuetryCatch returns one — and Try is exactly that idea as a type: Success(value) or Failure(exception), pattern-matchable, with getOrElse as the fallback shorthand. The imperative try/catch/finally also exists, but Try is the shape that composes with everything else on this page.
Pipes → Method Chains
The dot is the pipe
measurements <- c(12, 7, 25, 31, 8) measurements |> (\(values) values[values > 10])() |> sort() |> head(3) |> print()
object Main { def main(args: Array[String]): Unit = { val measurements = Vector(12, 7, 25, 31, 8) val result = measurements .filter(_ > 10) .sorted .take(3) println(result) } }
No pipe operator is needed because every step is a method returning a new collection — the dot does what |> does, without the unary-function contortions R’s pipe needs for filtering. This is the same left-to-right sentence dplyr taught, and it is the native shape of the language rather than an operator bolted on.
The Ecosystem Map (Spark)
The ecosystem map
# The R road into big data... library(sparklyr) # dplyr verbs over a Spark cluster library(dplyr) # spark_connect(), then filter/mutate/summarize as usual # — every verb is TRANSLATED to Spark behind the scenes # install.packages("sparklyr")
// ...and the native side of the same engine: import org.apache.spark.sql.SparkSession // SparkSession.builder.getOrCreate(), then // dataset.filter(...).groupBy(...).agg(...) — no translation layer // Breeze — vectors, matrices, linear algebra (numpy/Matrix territory) // Smile — statistics and machine learning on the JVM // Almond — a Scala kernel for Jupyter notebooks // Managed with sbt (build.sbt) or scala-cli, not install.packages
The reason this page exists: Spark is written in Scala, and sparklyr is a translation layer over it — moving to Scala removes the translation, unlocks the typed Dataset API (case-class rows, compile-checked columns), and makes the cluster’s full API surface available the day it ships. Around it: Breeze for linear algebra, Smile for statistics and machine learning, Almond to keep the notebook workflow. Both cells are import inventories, shown display-only.