galaaz 2.1.8 → 2.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Rakefile +20 -2
  4. data/bin/check_gemfile_lock_version +46 -0
  5. data/bin/release_bump +26 -0
  6. data/blogs/README.md +4 -0
  7. data/blogs/galaaz_2_0/galaaz_2_0.Rmd +385 -0
  8. data/blogs/galaaz_2_0/galaaz_2_0.md +409 -0
  9. data/blogs/galaaz_2_0/galaaz_2_0.tex +756 -0
  10. data/blogs/galaaz_2_0/images/galaaz-header.png +0 -0
  11. data/blogs/galaaz_2_0/images/galaaz-lockup-stacked.png +0 -0
  12. data/blogs/galaaz_ggplot/galaaz_ggplot.Rmd +14 -1
  13. data/blogs/galaaz_ggplot/galaaz_ggplot.md +123 -103
  14. data/blogs/galaaz_ggplot/galaaz_ggplot.tex +60 -23
  15. data/blogs/galaaz_ggplot/images/galaaz-lockup-stacked.png +0 -0
  16. data/blogs/gknit/gknit.Rmd +16 -1
  17. data/blogs/gknit/gknit.md +13 -1
  18. data/blogs/gknit/gknit.tex +64 -23
  19. data/blogs/gknit/gknit_files/figure-html/bubble-1.png +0 -0
  20. data/blogs/gknit/gknit_files/figure-html/diverging_bar.png +0 -0
  21. data/blogs/gknit/gknit_files/figure-latex/bubble-1.png +0 -0
  22. data/blogs/gknit/images/galaaz-lockup-stacked.png +0 -0
  23. data/blogs/manual/images/galaaz-lockup-stacked.png +0 -0
  24. data/blogs/manual/manual.Rmd +32 -11
  25. data/blogs/manual/manual.md +30 -23
  26. data/blogs/manual/manual.tex +88 -66
  27. data/blogs/manual/manual_files/figure-html/bubble-1.png +0 -0
  28. data/blogs/manual/manual_files/figure-latex/bubble-1.png +0 -0
  29. data/blogs/nse_dplyr/images/galaaz-lockup-stacked.png +0 -0
  30. data/blogs/nse_dplyr/nse_dplyr.Rmd +14 -1
  31. data/blogs/nse_dplyr/nse_dplyr.md +697 -649
  32. data/blogs/nse_dplyr/nse_dplyr.tex +61 -24
  33. data/blogs/oh_my/images/galaaz-lockup-stacked.png +0 -0
  34. data/blogs/oh_my/oh_my.Rmd +14 -1
  35. data/blogs/oh_my/oh_my.md +36 -26
  36. data/blogs/oh_my/oh_my.tex +95 -58
  37. data/blogs/r_on_rails_ledger/images/00_portfolio_page.png +0 -0
  38. data/blogs/r_on_rails_ledger/images/01_results_panel.png +0 -0
  39. data/blogs/r_on_rails_ledger/images/02_density_tail_risk.png +0 -0
  40. data/blogs/r_on_rails_ledger/images/03_mc_cone.png +0 -0
  41. data/blogs/r_on_rails_ledger/images/04_rolling_var.png +0 -0
  42. data/blogs/r_on_rails_ledger/images/galaaz-lockup-stacked.png +0 -0
  43. data/blogs/r_on_rails_ledger/r_on_rails_ledger.Rmd +354 -0
  44. data/blogs/r_on_rails_ledger/r_on_rails_ledger.md +365 -0
  45. data/blogs/r_on_rails_ledger/r_on_rails_ledger.tex +670 -0
  46. data/blogs/ruby_plot/images/galaaz-lockup-stacked.png +0 -0
  47. data/blogs/ruby_plot/ruby_plot.Rmd +14 -1
  48. data/blogs/ruby_plot/ruby_plot.md +11 -1
  49. data/blogs/ruby_plot/ruby_plot.tex +60 -23
  50. data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_with_jitter.png +0 -0
  51. data/blogs/ruby_plot/ruby_plot_files/figure-html/final_violin_plot.png +0 -0
  52. data/blogs/ruby_plot/ruby_plot_files/figure-html/violin_with_jitter.png +0 -0
  53. data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_with_jitter.png +0 -0
  54. data/blogs/ruby_plot/ruby_plot_files/figure-latex/final_violin_plot.png +0 -0
  55. data/blogs/ruby_plot/ruby_plot_files/figure-latex/violin_with_jitter.png +0 -0
  56. data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/facets_with_jitter.png +0 -0
  57. data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/final_violin_plot.png +0 -0
  58. data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/violin_with_jitter.png +0 -0
  59. data/lib/galaaz/cli.rb +47 -3
  60. data/logos/icon-font/README.md +27 -0
  61. data/logos/icon-font/build_font.py +130 -0
  62. data/logos/icon-font/galaaz-mark.svg +34 -0
  63. data/script/omarchy/README.md +8 -1
  64. data/script/omarchy/fonts/galaaz.ttf +0 -0
  65. data/script/omarchy/install-galaaz.sh +8 -1
  66. data/script/omarchy/omarchy-menu.jsonc +21 -9
  67. data/sty/galaaz-header.png +0 -0
  68. data/sty/galaaz-headers-from-p3.tex +4 -0
  69. data/sty/galaaz.sty +54 -23
  70. data/version.rb +1 -1
  71. metadata +30 -9
  72. data/blogs/galaaz_ggplot/galaaz_ggplot.log +0 -745
  73. data/blogs/gknit/gknit_files/gknit_files/figure-latex/bubble-1.png +0 -0
  74. data/blogs/manual/manual.log +0 -1530
  75. data/blogs/manual/manual_files/manual_files/figure-latex/bubble-1.png +0 -0
  76. data/blogs/nse_dplyr/nse_dplyr.log +0 -824
  77. data/blogs/oh_my/oh_my.log +0 -974
  78. data/blogs/ruby_plot/ruby_plot.log +0 -887
@@ -0,0 +1,409 @@
1
+ ---
2
+ title: "Galaaz 2.0: Ruby meets real GNU R (again)"
3
+ subtitle: "NewBridge, JRuby or CRuby, CRAN/Bioconductor, and Arrow-shaped data paths"
4
+ author: "Rodrigo Botafogo"
5
+ tags: [Galaaz, "Galaaz 2.0", Ruby, R, JRuby, CRuby, "GNU R", NewBridge, Arrow, knitr, gknit]
6
+ date: "2026"
7
+ output:
8
+ html_document:
9
+ self_contained: true
10
+ keep_md: true
11
+ toc: true
12
+ toc_float: true
13
+ toc_depth: 2
14
+ number_sections: true
15
+ includes:
16
+ before_body: _logo_before_body.html
17
+ pdf_document:
18
+ includes:
19
+ in_header:
20
+ - "../../sty/galaaz.sty"
21
+ - "../../sty/galaaz-headers-from-p3.tex"
22
+ keep_tex: yes
23
+ number_sections: yes
24
+ toc: true
25
+ toc_depth: 2
26
+ fontsize: 11pt
27
+ ---
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+ # Introduction
36
+
37
+ Ruby is excellent for web apps, orchestration, and expressive object models.
38
+ It has never matched **R** for statistics, Bioconductor pipelines, or the
39
+ depth of CRAN. **Python** closed a similar gap with NumPy, pandas, and a
40
+ huge scientific ecosystem. For Ruby, the practical answer is not to
41
+ reimplement that world — it is to **drive real R** from idiomatic Ruby.
42
+
43
+ **Galaaz** is that coupling. An earlier line of work ran on Oracle’s
44
+ **GraalVM** with **TruffleRuby** and **FastR** in one JVM. **Galaaz 2.0**
45
+ is a different architecture: **JRuby or CRuby** talk to **standard GNU R**
46
+ over a process bridge we call **NewBridge**. You keep CRAN and Bioconductor
47
+ (including compiled packages), and you keep a normal Ruby toolchain.
48
+
49
+ This post is the 2.0 story: what changed, how the bridge works, how to pin
50
+ R versions in containers, how data moves (including **Apache Arrow**), and
51
+ how to keep the bridge free for long R jobs. Deep API detail lives in the
52
+ [Galaaz Manual](https://rbotafogo.github.io/galaaz/); here we stay at
53
+ blog length with a few runnable sketches.
54
+
55
+ # What changed
56
+
57
+ | | Historical stack | Galaaz 2.0 |
58
+ |---|---|---|
59
+ | Ruby | TruffleRuby (Graal) | **JRuby** or **CRuby** |
60
+ | R | FastR (subset of GNU R) | **GNU R** (CRAN / Bioconductor) |
61
+ | Coupling | Same-JVM interop | **NewBridge** (separate processes) |
62
+ | Install | Special Graal distribution | `gem install` + compile gatekeeper |
63
+ | Legacy tools | `grun`, polyglot `gknit-draft` | `bin/galaaz-ruby`, `bin/gknit` |
64
+
65
+ Ideas from the older articles still matter — ggplot layers, dplyr pipes,
66
+ gKnit literate docs — but the **engine** underneath is different. If you
67
+ land on a 2018 “Ruby + R on GraalVM” post, treat the plots and narrative
68
+ as inspiration and this post (plus the manual) as the current stack.
69
+
70
+ # NewBridge in one picture
71
+
72
+ Galaaz 2.0 does **not** embed Renjin or FastR. Roughly:
73
+
74
+ 1. Your **Ruby** process (JRuby or CRuby) loads Galaaz.
75
+ 2. A native **gatekeeper** (`ext/new_bridge`, Rcpp / C++) mediates the protocol.
76
+ 3. A **GNU R** process evaluates calls, holds large objects, and runs packages.
77
+
78
+ Ruby stays Ruby: `R.mean`, `E.aes`, method chaining, classes and modules.
79
+ R stays R: the same `lm`, `DESeq2`, or `ggplot2` you would run in RStudio.
80
+ Large panels should usually **stay in R**; Ruby orchestrates and pulls
81
+ small results (Remote Control). Opt in to unbox scalars when you need a
82
+ plain Ruby number (`>> 0` for length-1 vectors is the common 2.0 idiom).
83
+
84
+ A minimal session looks like this:
85
+
86
+
87
+ ``` ruby
88
+ require 'galaaz'
89
+
90
+ puts R.sum(R.c(1, 2, 3, 4, 5))
91
+ mtcars = ~R[:mtcars]
92
+ puts mtcars.dim
93
+ puts R.summary(mtcars.mpg)
94
+ ```
95
+
96
+ ```
97
+ ## [1] 15
98
+ ## [1] 32 11
99
+ ## Min. 1st Qu. Median Mean 3rd Qu. Max.
100
+ ## 10.40 15.43 19.20 20.09 22.80 33.90
101
+ ```
102
+
103
+ Callbacks still exist: Ruby procs can be registered so R code can call
104
+ back into Ruby when a package expects an R function. That path is
105
+ documented in the manual; the important product point is that **real
106
+ GNU R** is on the other end of the wire.
107
+
108
+ # Either Ruby engine
109
+
110
+ NewBridge is tested on both:
111
+
112
+ * **JRuby** (e.g. 10.x + JDK 21) — real OS threads, JVM ecosystem, natural
113
+ fit for Rails under load.
114
+ * **CRuby** (e.g. 3.3+) — MRI gem ecosystem, familiar for many Ruby shops.
115
+
116
+ The public API is the same. Launchers honor **`GALAAZ_RUBY`**.
117
+ Use **`bin/galaaz-ruby`**, or **`bin/galaaz-jruby`** to force JRuby.
118
+ Prefer **JRuby** when you want parallel Ruby threads feeding R;
119
+ prefer **CRuby** when your app and gems already live there. Arrow
120
+ helpers need engine-specific native bits (Java Arrow + `JAVA_OPTS`
121
+ nio opens on JRuby; Arrow GLib + `red-arrow` on CRuby) — see the
122
+ manual’s Arrow section.
123
+
124
+ ```bash
125
+ # From a checkout or installed layout:
126
+ bin/galaaz-ruby -e \
127
+ 'require "galaaz"; puts R.R__version[["version.string"]]'
128
+ GALAAZ_RUBY=jruby bin/galaaz-ruby -e \
129
+ 'require "galaaz"; puts R.pi'
130
+ ```
131
+
132
+ # Multiple R versions via containers
133
+
134
+ Because R is a **separate process**, the Ruby gem does not hard-wire one
135
+ R build. Whatever `R` / `Rscript` is on `PATH` (and can load the
136
+ gatekeeper) is the R you drive. That makes **containers** a clean way to
137
+ pin versions:
138
+
139
+ * Image A: Ubuntu + **R 4.3** + system libs for your Bioconductor set.
140
+ * Image B: same app code + **R 4.4** (or a rocker image) for comparison.
141
+ * Your Galaaz gem and Ruby code stay the same; only the image’s R changes.
142
+
143
+ Sketch (conceptual):
144
+
145
+ ```bash
146
+ # Dev against the R that the image provides
147
+ docker run --rm -it myorg/galaaz-r43 \
148
+ bin/galaaz-ruby script/analysis.rb
149
+
150
+ docker run --rm -it myorg/galaaz-r44 \
151
+ bin/galaaz-ruby script/analysis.rb
152
+ ```
153
+
154
+ Inside each image, `R --version` differs; NewBridge still speaks the same
155
+ protocol. For a first taste without building your own Dockerfile, the
156
+ repo ships **try** images and runners:
157
+
158
+ ```bash
159
+ # From a Galaaz checkout
160
+ ./docker/try-gstudio/run.sh # JRuby + gstudio-oriented image
161
+ ./docker/try-cruby/run.sh # CRuby try image
162
+ ```
163
+
164
+ **Cold-install** proofs (`docker/cold-install`,
165
+ `docker/cold-install-cruby`) install the gem on a throwaway Ubuntu and
166
+ compile the gatekeeper — useful when you want “empty machine → working
167
+ bridge” confidence. Multi-runtime Docker R matrices are still evolving
168
+ (slow integration coverage); the sketch above is the pattern to build on.
169
+
170
+ # Moving data: Apache Arrow
171
+
172
+ Crossing a process boundary means you must be honest about **copies**.
173
+ Galaaz’s Arrow roadmap has three stages:
174
+
175
+ | Stage | Idea | Status |
176
+ |---|---|---|
177
+ | **A** | Copy columnar data into R; return an R-side Arrow/proxy handle | Available (`from_ruby_batches`, `table_from`, …) |
178
+ | **B** | Same-machine **IPC file**: bulk bytes stay off the MsgPack bridge; only a **file path** crosses NewBridge | Shipped (two directions: B1 and B2) |
179
+ | **C** | Shared-memory bus (true shared RAM) | Future |
180
+
181
+ Do **not** read Stage A as “zero-copy shared RAM.” Stage A is “build the
182
+ table in R efficiently, then Remote-Control it.” Stage B is still a
183
+ file/`mmap` handoff on one machine—not Stage C—but it keeps large
184
+ columns out of the control plane.
185
+
186
+ ## Stage B in plain words
187
+
188
+ **IPC** here means Apache Arrow’s **Inter-Process Communication** file
189
+ format: a portable columnar dump on disk (often under `/dev/shm` so it
190
+ lives in RAM-backed tmpfs). Ruby and R never shove megabytes through
191
+ NewBridge; they agree on a path, then each side’s Arrow stack
192
+ reads or writes that file.
193
+
194
+ Stage B splits by **direction**:
195
+
196
+ * **B1 — Ruby → R (ingest).** Ruby writes an IPC file
197
+ (`Galaaz::ArrowIpc.write` / `write_batches`). R opens it with
198
+ `R::Arrow.open_ipc(path)` and gets a table proxy for dplyr or
199
+ modeling. After R has materialised the table, Ruby can
200
+ `Galaaz::ArrowIpc.release(path)` (unlink the scratch file).
201
+ * **B2 — R → Ruby (export).** After analytics in R, `R::Arrow.write_ipc`
202
+ writes another IPC file and returns its path over the bridge. Ruby
203
+ reads columns or row hashes with `Galaaz::ArrowIpc.read` /
204
+ `read_batches`—handy when the next step is a DB write or an API
205
+ payload in Ruby.
206
+
207
+ Needs: R package `arrow`, plus a Ruby writer/reader (CRuby:
208
+ **red-arrow**; JRuby: Arrow Java JARs). Specs skip when those are
209
+ missing; the sketch below does the same.
210
+
211
+ ## Stage B sketch (B1 ingest + B2 export)
212
+
213
+ This is the path to prefer for large same-machine handoffs. Ruby
214
+ threads build row batches; **B1** writes one IPC file and R opens it by
215
+ path; dplyr summarises in R; **B2** writes the result IPC and Ruby
216
+ reads row hashes. NewBridge only carries paths.
217
+
218
+
219
+ ``` ruby
220
+ ipc_ok = Galaaz::ArrowIpc.available? &&
221
+ (R::Support.eval(
222
+ "requireNamespace('arrow', quietly=TRUE) && " +
223
+ "requireNamespace('dplyr', quietly=TRUE)") == true)
224
+ unless ipc_ok
225
+ puts '(Skip: need Arrow IPC backend + R arrow/dplyr.)'
226
+ else
227
+ thread_count = 2
228
+ rows_per_thread = 250
229
+ group_count = 5
230
+ batches = []
231
+ mutex = Mutex.new
232
+ threads = []
233
+
234
+ thread_count.times do |tid|
235
+ threads << Thread.new do
236
+ start = tid * rows_per_thread
237
+ local = (start...(start + rows_per_thread)).map do |i|
238
+ {
239
+ id: i,
240
+ grp: "g#{i % group_count}",
241
+ value: (i % 17) + 1,
242
+ weight: ((i % 5) + 1) * 0.5
243
+ }
244
+ end
245
+ mutex.synchronize { batches << local }
246
+ end
247
+ end
248
+ threads.each(&:join)
249
+
250
+ paths = []
251
+ begin
252
+ # B1: Ruby → IPC file → R table proxy
253
+ # write_batches wants flat row Hashes (not nested batches)
254
+ in_path = Galaaz::ArrowIpc.write_batches(batches.flatten)
255
+ paths << in_path
256
+ tbl = R::Arrow.open_ipc(in_path)
257
+ puts "R class: #{tbl.rclass}"
258
+ puts "IPC in: #{File.basename(in_path)}"
259
+
260
+ grouped = R.dplyr___group_by(tbl, :grp)
261
+ summed = R.dplyr___summarise(
262
+ grouped,
263
+ n: E.n(),
264
+ total: E.sum(:value),
265
+ wsum: E.sum(R[:value] * R[:weight])
266
+ )
267
+
268
+ # B2: R → IPC file → Ruby row hashes
269
+ out_path = R::Arrow.write_ipc(summed)
270
+ paths << out_path
271
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
272
+ puts "IPC out: #{File.basename(out_path)}"
273
+ rows.first(5).each do |r|
274
+ puts "#{r[:grp]} n=#{r[:n]} " +
275
+ "total=#{r[:total]} wsum=#{r[:wsum]}"
276
+ end
277
+
278
+ total_n = rows.map { |r| r[:n].to_i }.sum
279
+ expect_n = thread_count * rows_per_thread
280
+ puts "Sum of n (expect #{expect_n}): #{total_n}"
281
+ ensure
282
+ paths.each { |p| Galaaz::ArrowIpc.release(p) }
283
+ end
284
+ end
285
+ ```
286
+
287
+ ```
288
+ ## R class: Table
289
+ ## IPC in: galaaz_ipc_35027_0eb039500ea01e8a.arrow
290
+ ## IPC out: galaaz_ipc_35027_2e528c6c1bf2152d.arrow
291
+ ## g0 n=100 total=897 wsum=448.5
292
+ ## g1 n=100 total=895 wsum=895.0
293
+ ## g2 n=100 total=893 wsum=1339.5
294
+ ## g3 n=100 total=891 wsum=1782.0
295
+ ## g4 n=100 total=889 wsum=2222.5
296
+ ## Sum of n (expect 500): 500
297
+ ```
298
+
299
+ What to notice: `write_batches` / `open_ipc` is **B1**; after dplyr,
300
+ `write_ipc` / `read_batches` is **B2**. Specs:
301
+ `arrow_ipc_handoff_spec.rb`, `arrow_ipc_export_spec.rb`. Deeper notes:
302
+ `Documentation/ROADMAP_ARROW_RUBY_R.md`.
303
+
304
+ ## Stage A (still useful)
305
+
306
+ Stage A copies batches straight into R with
307
+ `R::Arrow.from_ruby_batches`—no IPC file. Fine for modest tables or
308
+ when you do not have a Ruby Arrow writer installed. Same threaded
309
+ shape as above, one call instead of B1:
310
+
311
+
312
+ ``` ruby
313
+ arrow_ok = R::Support.eval(
314
+ "requireNamespace('arrow', quietly=TRUE) && " +
315
+ "requireNamespace('dplyr', quietly=TRUE)")
316
+ unless arrow_ok == true
317
+ puts '(Skip: need arrow + dplyr in R.)'
318
+ else
319
+ batches = [
320
+ [{ id: 1, grp: 'a', value: 10, weight: 1.0 },
321
+ { id: 2, grp: 'b', value: 20, weight: 0.5 }],
322
+ [{ id: 3, grp: 'a', value: 30, weight: 1.5 }]
323
+ ]
324
+ tbl = R::Arrow.from_ruby_batches(batches)
325
+ puts "R class: #{tbl.rclass}"
326
+ grouped = R.dplyr___group_by(tbl, :grp)
327
+ summarised = R.dplyr___summarise(
328
+ grouped, n: E.n(), total: E.sum(:value))
329
+ out = R.dplyr___collect(summarised)
330
+ puts R.as__data__frame(out)
331
+ end
332
+ ```
333
+
334
+ ```
335
+ ## R class: Table
336
+ ## grp n total
337
+ ## 1 a 2 40
338
+ ## 2 b 1 20
339
+ ```
340
+
341
+ Larger Stage A demos live in `arrow_from_ruby_batches_spec.rb` and
342
+ `arrow_large_pipeline_spec.rb` (under `specs/` / `slow-specs/`).
343
+
344
+ # Keeping the bridge free
345
+
346
+ Two different “don’t block” stories:
347
+
348
+ * **`R::Async` / `R.eval_r_async`** — free a **Ruby** thread while the
349
+ **same** bridge R process works. Good for short/medium calls when you
350
+ still want one R session.
351
+ * **`R::Job`** — run heavy work in a **child `Rscript`**. The bridge
352
+ stays free for other traffic. Package installs
353
+ (`R.install_and_loads`) use this path so `make`/`gcc` do not wedge
354
+ the gatekeeper.
355
+
356
+
357
+ ``` ruby
358
+ begin
359
+ coef = R::Job.eval(<<~R) { |job| job.load_rds }
360
+ fit <- lm(mpg ~ wt, data = mtcars)
361
+ saveRDS(unname(coef(fit)), result_path)
362
+ R
363
+ puts coef
364
+ rescue => e
365
+ puts e.class.to_s
366
+ e.message.to_s.scan(/.{1,68}/).each { |line| puts line }
367
+ end
368
+ ```
369
+
370
+ ```
371
+ ## [1] 37.285126 -5.344472
372
+ ```
373
+
374
+ Rule of thumb: short interactive analytics on the bridge; installs and
375
+ multi-minute fits in **`R::Job`**. Details and timeouts
376
+ (`GALAAZ_INSTALL_TIMEOUT_SEC`, `GALAAZ_JOBS_DIR`) are in the manual.
377
+
378
+ # What stayed the same
379
+
380
+ Galaaz 2.0 is a new **runtime**, not a new plotting language. These still
381
+ apply:
382
+
383
+ * **ggplot2** from Ruby — see *Ruby Plotting with Galaaz* /
384
+ *Beautiful Ruby Plots*.
385
+ * **dplyr / NSE** — see *Non Standard Evaluation in dplyr with Galaaz*.
386
+ * **Literate docs** — **gKnit** still wraps knitr so Ruby chunks render
387
+ next to R (HTML/PDF).
388
+ * **Classes, modules, procs** — the *oh my* S4 comparison still teaches
389
+ structure; unwrap and bridge details follow 2.0 rules.
390
+
391
+ R-on-Rails (Ruby web + R science) is the product framing: Rails (or any
392
+ Ruby app) orchestrates; GNU R computes.
393
+
394
+ # Conclusion
395
+
396
+ **Galaaz 2.0** bets on **boring, powerful defaults**: standard Ruby
397
+ engines, standard GNU R, a clear process bridge, containers to pin R,
398
+ and Arrow-shaped paths for larger tables — with zero-copy shared memory
399
+ still on the roadmap. The Graal-era prototype proved the *idea*; 2.0
400
+ makes the *ecosystem* reachable.
401
+
402
+ ## Further reading
403
+
404
+ * Manual (GitHub Pages): https://rbotafogo.github.io/galaaz/
405
+ * Arrow roadmap: `Documentation/ROADMAP_ARROW_RUBY_R.md`
406
+ * Repo blogs under `blogs/` (ggplot, dplyr, gKnit, oh my, manual)
407
+ * Try images: `docker/try-gstudio`, `docker/try-cruby`
408
+ * Older Graal-era plot narrative (historical): search
409
+ “Ruby Plotting with Galaaz in GraalVM” on Towards Data Science