Skip to content

Ruby cells

A ```ruby block is a cell: Ruby on rbgo with the go-scikit bindings, sharing its variables with the Go cells.

A ```ruby block is a Ruby cell (go-notebook v0.3.0 and later). The Ruby is rbgo, a Ruby bytecode VM written in Go, with the go-scikit bindings registered over the same Go libraries a Go cell imports:

RubyGo libraryCounterpart
Statsgo-statistics/statsscipy.stats
Specialgo-numerics/specialscipy.special
Optimizego-numerics/optimizescipy.optimize
Integratego-numerics/integratescipy.integrate
DataFramego-dataframe/dataframepandas
Plotgo-plot/plotmatplotlib.pyplot
NDArraygo-ndarray/ndarray (rbgo’s own)numpy.ndarray

Every binding calls the Go function it is named after; the bindings’ README lists every name.

The kernel adds github.com/go-scikit/ruby to the notebook’s work module by itself, and builds the worker with -tags rbgo_core (the Ruby language and its numeric stack, without gems). A notebook needs nothing more than go-notebook.

A mixed notebook

A Go cell makes the data, a Ruby cell fits it with a go-scikit binding, and a Go cell reads the fit back (the file’s prose is left out here):

```go
xs := []float64{2.1, 3.4, 1.9, 5.6, 4.4, 3.3, 2.8, 4.9}
```

```ruby
fit = Stats::Normal.fit(xs)
loc, scale = fit.loc, fit.scale
ks = Stats.ks_1samp(xs) { |x| fit.cdf(x) }
pvalue = ks.pvalue
fit
```

```go
import "fmt"

fmt.Printf("loc %.4f, scale %.4f, KS p-value %.4f\n", loc, scale, pvalue)
```

In a module whose go.mod has tool github.com/go-notebook/notebook/cmd/notebook and requires github.com/go-notebook/notebook v0.3.0:

$ go tool notebook run first.md
── cell 0 (line 6, ran in 0s)
── cell 1 (line 13, ran in 366µs)
   #<Stats::Normal loc=3.55, scale=1.2359207094308275>
── cell 2 (line 23, ran in 3µs)
   loc 3.5500, scale 1.2359, KS p-value 0.9381

In the Ruby cell xs is an Array of Floats; in the last Go cell loc, scale and pvalue are float64s. (A real transcript, from go-notebook v0.3.0 with go-scikit/ruby v0.1.0; the line numbers are those of the full file.) A larger example, with an NDArray and a plot, is examples/mixed.md.

The rules

Ruby cells follow the rules of Go cells: one definition per name, no assignment to another cell’s variable, and a name starting with _ stays in its cell. The names come from rbgo’s own parser: a cell defines the locals it assigns at its top level, and the methods, constants, classes and modules it defines there; it uses the names it reads before assigning them.

The Ruby cells share one interpreter, so their top-level locals persist from cell to cell. Dependencies, caching and re-runs work across the two languages as for Go alone: a Ruby cell runs again only when it, or a cell it reads, changed.

A cell that ends in an expression shows its value: a Plot figure as SVG, anything else as its inspect (an NDArray in NumPy’s form). An exception fails the cell with file:line: message (Class) at the notebook’s line, followed by the backtrace frames in the notebook (twelve at most).

How values cross between Go and Ruby

Only variables cross. A Go cell sees a Ruby cell’s top-level locals, not its methods, constants or classes, nor a local named like a Go keyword or predeclared identifier (len, type, int). A Ruby cell sees a Go cell’s variables, not its funcs or types.

Go to Ruby. Before a Ruby cell runs, each Go variable it reads becomes a Ruby local of the same name (a constant when the name starts with an upper-case letter):

  • numbers, strings and booleans as such;
  • slices as Arrays, maps as Hashes;
  • an *ndarray.Array as the NDArray itself, not a copy;
  • any other value (a *plot.Figure, a struct) as an object that comes back to Go as itself.

Ruby to Go. After a Ruby cell runs, each local it assigns becomes a Go variable whose type is the type of its value:

Ruby valueGo type
nilany (nil)
true, falsebool
Integerint64 (*big.Int when it does not fit)
Floatfloat64
Stringstring
Symbolruby.Symbol
Complexcomplex128
Rational*big.Rat
NDArray*ndarray.Array, the same array
Array of all Floats, all Integers, all Strings, all booleans[]float64, []int64, []string, []bool
any other Array[]any
Hash with String keysmap[string]any
Hash with Symbol keysmap[ruby.Symbol]any
any other Hashmap[any]any
a go-scikit objectthe Go value it wraps: *plot.Figure, stats.TTestResult, *optimize.Result, …
any other Ruby objectruby.Value, which a Go cell can Call

The type comes from the value, so it is known once the Ruby cell has run. A Go cell that reads a Ruby variable whose type is not known yet waits: the Ruby cell runs, then the Go cell is compiled with the type and runs, in the same run. When a later edit changes the type, only the Go cells that read the variable are compiled again.

Limits

A notebook is a program, and it runs with your rights. A Ruby cell can reach what a Go cell can: files through File, IO and Dir, other processes through Process, the environment through ENV, and the bindings that write files (DataFrame#write_csv, Plot::Figure#save_svg). The rbgo_core build leaves out the network libraries (socket, net/*, openssl): requiring one raises LoadError. Run only notebooks you trust.

Every cell runs in the worker, a process of its own, so the kernel survives whatever a cell does:

What the cell doesWhat happens
runs too longwith -timeout 30s, its worker is killed and the cell fails with “stopped: the cell ran longer than 30s”; the next run starts a new worker
holds too much memorywith -memory 512 (MiB), the garbage collector keeps under the limit as long as it can, then the worker is stopped and the cell fails with “stopped: the cell went over its memory limit”
recurses without endRuby raises SystemStackError, shown with twelve frames
asks for an impossible allocation ('x' * 2**62)Ruby raises NoMemoryError
calls exit or exit!a SystemExit fails the cell; the worker lives on
kills its own process (Process.kill, not on Windows; os.Exit in Go)the cell says so, and the next run restarts the worker

Both limits apply to Go and Ruby cells alike and are off by default. The memory limit counts the Go heap, sampled every 20 ms; memory held outside it (a large stack, a mapped file) is bounded only by the operating system.

Created on by tannevaled

Modified on by tannevaled