きしださんのかわいいリレーショナルデータベースをRubyで写経した

nowokay.hatenablog.com

きしださんのかわいいリレーショナルデータベースの最初のバージョンを写経してみました。 この記事、もう8年前なんですね。ついこないだ読んだような気がしていましたが……。

簡単なものだったら自作できないかなと以前から思っていたんですよね。 最近やっと重い腰を上げて着手したのですが、 そこできしださんの記事のことをふと思い出し、 参考のために写経してみました。 そういえば、そもそもリレーショナルデータベース自作について考えるようになったきっかけの1つもきしださんの記事だった気がします。


一度そのまま書いたあと、いろいろいじってみました。 それにより非効率になっている部分もありますが、ひとまず自分にとっての理解のしやすさを優先しました。


以下の kawaii_rdb.rbtest_kawaii_rdb.rb を同じディレクトリに置いて

ruby test_kawaii_rdb.rb

で実行できます。


kawaii_rdb.rb

class Database
  @@tables = {}

  def self.tables
    @@tables
  end
end

class Relation
  attr_reader :columns, :tuples

  def initialize(columns, tuples)
    @columns = columns
    @tuples = tuples
  end

  def valid_index?(index)
    0 <= index && index < @columns.size
  end

  # みつからなかったときは属性数(インデックスからあふれる)を返す
  def column_index(name)
    idx = @columns.index { |column| column.name == name }
    idx || @columns.size
  end

  def to_s
    all_rows = [@columns] + @tuples.map(&:values)

    all_rows.map { |values|
      inner =
        values
          .map { |v| v.nil? ? "null" : v }
          .join(" | ")

      "| #{inner} |\n"
    }.join
  end
end

class Query < Relation
  def self.from(table_name)
    tbl = Database.tables[table_name]

    new_columns =
      tbl.columns.map { |column|
        Column.new(column.name, table_name)
      }

    Query.new(new_columns, tbl.tuples)
  end

  def filter_tuple(tuple, column_names)
    values =
      column_names.map { |column_name|
        idx = column_index(column_name)

        if tuple.valid_index?(idx)
          tuple.values[idx]
        else
          nil
        end
      }

    Tuple.new(values)
  end

  def select(*column_names)
    new_columns =
      column_names.map { |column_name|
        Column.new(column_name)
      }

    new_tpls =
      tuples.map { |tuple|
        filter_tuple(tuple, column_names)
      }

    Query.new(new_columns, new_tpls)
  end

  def join_tuple(tuple_l, tuple_r, num_columns)
    new_tpl = tuple_l.dup

    if tuple_r
      tuple_r.values.each { |v|
        new_tpl.values << v
      }
    else
      while new_tpl.size < num_columns
        new_tpl.values << nil
      end
    end

    new_tpl
  end

  def left_join(table_name, matching_field)
    tbl_r = Database.tables[table_name]

    # 属性の作成
    new_columns =
      @columns +
      tbl_r.columns.map { |column| Column.new(column.name, table_name) }

    # 値の作成
    left_column_idx = column_index(matching_field)
    right_column_idx = tbl_r.column_index(matching_field)

    if valid_index?(left_column_idx) && tbl_r.valid_index?(right_column_idx)
      # 両方に該当フィールドあり
    else
      # 該当フィールドがない場合は値の結合をしない
      return Query.new(new_columns, [])
    end

    # 結合処理
    new_tpls =
      @tuples.map { |tpl_l|
        # 結合対象のフィールドを探す(左)
        left_value = tpl_l.values[left_column_idx]

        # 結合対象のタプルを探す
        tpl_r =
          tbl_r.tuples
            .find { |iter_tpl_r|
              # 結合対象のフィールドを探す(右)
              iter_tpl_r.values[right_column_idx] == left_value
            }

        join_tuple(tpl_l, tpl_r, new_columns.size)
      }

    Query.new(new_columns, new_tpls)
  end

  def less_than(column_name, value)
    idx = column_index(column_name)

    unless valid_index?(idx)
      return Query.new(columns, [])
    end

    new_tpls = tuples.select { |tuple| tuple.values[idx] < value }
    
    Query.new(columns, new_tpls)
  end
end

class Table < Relation
  def initialize(name, columns)
    super(columns, [])
    @name = name
  end

  def self.create(name, column_names)
    columns = column_names.map { |column_name| Column.new(column_name) }
    tbl = Table.new(name, columns)
    Database.tables[name] = tbl
    tbl
  end

  def insert(*values)
    @tuples << Tuple.new(values)
    self
  end
end

class Tuple
  attr_reader :values

  def initialize(values)
    @values = values
  end

  def size
    @values.size
  end

  def valid_index?(index)
    0 <= index && index < size
  end
end

class Column
  attr_reader :name

  def initialize(name, parent = nil)
    @parent = parent
    @name = name
  end

  def to_s
    if @parent
      "#{@parent}.#{@name}"
    else
      @name
    end
  end
end

test_kawaii_rdb.rb

require_relative "./kawaii_rdb"
require "minitest/autorun"

class Test < Minitest::Test
  def setup
    # 商品テーブル
    shohin = Table.create(
      "shohin",
      %w(shohin_id shohin_name kubun_id price)
    )

    shohin
      .insert(1, "りんご"  , 1  , 300)
      .insert(2, "みかん"  , 1  , 130)
      .insert(3, "キャベツ", 2  , 200)
      .insert(4, "わかめ"  , nil, 250) # 区分が null
      .insert(5, "しいたけ", 3  , 180) # 該当区分なし

    # 区分テーブル
    kubun = Table.create(
      "kubun",
      %w(kubun_id kubun_name)
    )

    kubun
      .insert(1, "くだもの")
      .insert(2, "野菜"    )
  end

  # テーブル内容
  def test_table
    expected = <<~EXP
    | shohin_id | shohin_name | kubun_id | price |
    | 1 | りんご | 1 | 300 |
    | 2 | みかん | 1 | 130 |
    | 3 | キャベツ | 2 | 200 |
    | 4 | わかめ | null | 250 |
    | 5 | しいたけ | 3 | 180 |
    EXP

    assert_equal(
      expected,
      Database.tables["shohin"].to_s
    )
  end

  # クエリー経由でテーブル内容
  def test_from
    expected = <<~EXP
    | shohin.shohin_id | shohin.shohin_name | shohin.kubun_id | shohin.price |
    | 1 | りんご | 1 | 300 |
    | 2 | みかん | 1 | 130 |
    | 3 | キャベツ | 2 | 200 |
    | 4 | わかめ | null | 250 |
    | 5 | しいたけ | 3 | 180 |
    EXP

    assert_equal(
      expected,
      Query.from("shohin").to_s
    )
  end

  # 射影
  def test_select
    expected = <<~EXP
    | shohin_name | price |
    | りんご | 300 |
    | みかん | 130 |
    | キャベツ | 200 |
    | わかめ | 250 |
    | しいたけ | 180 |
    EXP

    assert_equal(
      expected,
      Query.from("shohin").select("shohin_name", "price").to_s
    )
  end

  # フィルター
  def test_filter
    expected = <<~EXP
    | shohin.shohin_id | shohin.shohin_name | shohin.kubun_id | shohin.price |
    | 2 | みかん | 1 | 130 |
    | 3 | キャベツ | 2 | 200 |
    | 5 | しいたけ | 3 | 180 |
    EXP

    assert_equal(
      expected,
      Query.from("shohin").less_than("price", 250).to_s
    )
  end

  # 結合
  def test_join
    expected = <<~EXP
    | shohin.shohin_id | shohin.shohin_name | shohin.kubun_id | shohin.price | kubun.kubun_id | kubun.kubun_name |
    | 1 | りんご | 1 | 300 | 1 | くだもの |
    | 2 | みかん | 1 | 130 | 1 | くだもの |
    | 3 | キャベツ | 2 | 200 | 2 | 野菜 |
    | 4 | わかめ | null | 250 | null | null |
    | 5 | しいたけ | 3 | 180 | null | null |
    EXP

    assert_equal(
      expected,
      Query.from("shohin").left_join("kubun", "kubun_id").to_s
    )
  end

  # 全部入り
  def test_all
    expected = <<~EXP
    | shohin_name | kubun_name | price |
    | みかん | くだもの | 130 |
    | しいたけ | null | 180 |
    EXP

    actual =
      Query
        .from("shohin")
        .left_join("kubun", "kubun_id")
        .less_than("price", 200)
        .select("shohin_name", "kubun_name", "price")
        .to_s

    assert_equal(expected, actual)
  end
end

参考

red-arrow: Arrow::Tableのデータを組み立てる

とりあえず最低限の流れが知りたかったので、int32 だけの簡単なデータでやってみました。

require "arrow"

# --------------------------------
# 列1 のデータを用意

builder = Arrow::Int32ArrayBuilder.new
builder.append(1)
builder.append(2)
array1 = builder.finish

p array1

# #<Arrow::Int32Array:0x557fac799a88 ptr=0x557fac442e80 [
#   1,
#   2
# ]>

# --------------------------------
# 列2 のデータを用意

# Arrow::XxxArray.new を使うと簡単
array2 = Arrow::Int32Array.new([11, 12])

p array2

# #<Arrow::Int32Array:0x557fac798ed0 ptr=0x557fac442fa0 [
#   11,
#   12
# ]>

# --------------------------------

col1_f = Arrow::Field.new("col1", :int32) # フィールド名、型
col2_f = Arrow::Field.new("col2", :int32)

fields = [col1_f, col2_f]
schema = Arrow::Schema.new(fields)

# --------------------------------

record_batch = Arrow::RecordBatch.new(
  schema,
  2, # 件数
  [array1, array2]
)

p record_batch

# #<Arrow::RecordBatch:0x557fac791720 ptr=0x557fac6c86b0 col1:   [
#     1,
#     2
#   ]
# col2:   [
#     11,
#     12
#   ]
# >

# --------------------------------
# Arrow::Table に変換

table = record_batch.to_table

p table

# #<Arrow::Table:0x557fac790618 ptr=0x557fac6c87a0>
#         col1    col2
# 0          1      11
# 1          2      12
バージョン:
  Ruby 2.7.1
  red-arrow 1.0.0

参考

consごっこ (2)



consのデータをメモリに置くとどうなるのか、というのを軽く試してみるつもりだったのが、 もうちょっと育ってしまいました。

C言語云々と言っているところはかなりうろ覚えで適当です。だいぶ忘れてます……。

※ また、既存のよく知られた何かに準拠している訳ではなく、思いつきで作った適当なものです。 実際の Lisp 処理系が以下のようになっているのかよく分かってません。 どうなってるんでしょうか。


  • メモリはただの配列
    • 添字がアドレス
    • 内容は整数(値またはアドレス)
  • 整数は1つのアドレスを占有する
  • consセルは car と cdr で隣接した2つのアドレスを占有する

初期状態:

(アドレス: 内容)
0: nil ... 0番地は nil を表す。変更しない。
1: nil ... 未使用。以下同じ
2: nil
...

assign_int(:a, 11) assign_int(:b, 22) を実行すると、空いている場所に値が入れられ、変数に束縛される。

C言語だと int a = 11; int b = 22; みたいな感じ?

0: nil
1: 11 ... a
2: 22 ... b
3: nil
...

値のアドレスを使って cons セルを作成。

cons セルの car部、cdr部には必ずアドレスを入れる(即値は入れない)。 car と cdr は隣接して配置する(car のアドレスの次の番地が cdr)。

C言語だと cons* c1 = cons(&a, &b); みたいな感じ?

0: nil
1: 11 ... a
2: 22 ... b
3: 1 ... c1 の car
4: 2 ... c1 の cdr
5: nil
...
  • c1 の car のアドレスは 3(内容=参照先アドレスは 1)
  • c1 の cdr のアドレスは 4(内容=参照先アドレスは 2)
  • それぞれの参照先アドレスを辿ると aの値=11, bの値=22 が得られる。

……というところから出発して、 ついでに free みたいなこともできる?   変数名の管理が必要?   ついでに GC ぽいものも作れる?   ……などと思いつきでいじってたら以下のようになりました。

  • $env で変数名と型(数またはcons)、アドレスの対応を管理
※ こういうイメージ
$env:
  a:
    type: int
    addr: 1
  b:
    type: int
    addr: 2
  c1:
    type: cons
    addr: 3 ... consセルの先頭アドレス
  • $in_use_flags で各番地ごとの使用・未使用を管理

    • 使用中の場合 true
    • 初期状態では 0 番地のみ true
  • unbind

    • free っぽい何か?
    • unbind(name) では $env からキー(変数名)を削除するだけ
  • sweep

    • GC っぽい何か?
    • 参照されている(=使われている)アドレスは $env を見るとすべて分かる
    • それ以外のアドレスのフラグを false にする
  • 領域の確保と変数の束縛

    • メモリを先頭から見ていって、必要なサイズの領域が未使用だったらそこを使う
      • 未使用かどうかは $in_use_flags を見て判断
    • 領域確保時に $in_use_flags のフラグを true に更新
    • allocate(size) すると、確保した領域の先頭のアドレスを返す
    • $env に登録(束縛)
class EnvItem
  attr_reader :type, :addr

  def initialize(type, addr)
    @type = type # :int | :cons
    @addr = addr
  end

  def to_s
    "(#{@type} #{@addr})"
  end
end

def all_free?(addrs)
  addrs.all? { |addr| $in_use_flags[addr] == false }
end

def find_free_addr(size)
  addrs =
    (1...$in_use_flags.size)
      .each_cons(size)
      .find { |addrs| all_free?(addrs) }

  if addrs.nil?
    raise "no free space"
  end

  addrs[0]
end

def allocate(size)
  addr = find_free_addr(size)
  (0...size).each { |offset|
    $in_use_flags[addr + offset] = true
  }
  addr
end

def assign_int(name, val)
  addr = allocate(1)
  $mem[addr] = val
  $env[name] = EnvItem.new(:int, addr)
  addr
end

def assign_cons(name, car_addr, cdr_addr)
  addr = allocate(2)
  $mem[addr    ] = car_addr
  $mem[addr + 1] = cdr_addr
  $env[name] = EnvItem.new(:cons, addr)
  addr
end

def unbind(name)
  $env.delete(name)
end

def sweep
  in_use_addrs =
    $env.values
      .flat_map { |item|
        case item.type
        when :int
          [item.addr]
        when :cons
          [item.addr, item.addr + 1]
        end
      }

  (1...$mem.size)
    .reject { |addr| in_use_addrs.include?(addr) }
    .each { |addr| $in_use_flags[addr] = false }
end

def name_to_addr(name)
  $env[name].addr
end

def dump_mem
  $mem.zip($in_use_flags).each_with_index { |x, i|
    val, in_use_flag = x
    puts "% 4d %s %s" % [i, in_use_flag ? "*" : " ", val.inspect]
  }
end

初期化:

MEM_SIZE = 14
$mem = Array.new(MEM_SIZE)
$in_use_flags = Array.new(MEM_SIZE){ false }

NIL_ADDR = 0
$mem[NIL_ADDR] = nil
$in_use_flags[NIL_ADDR] = true

$env = {}
# 変数 a, b, c を assign
assign_int(:a, 11)
assign_int(:b, 22)
assign_int(:c, 33)

unbind(:b) # ... まだフラグは true のままで、$env から消えるだけ
sweep() # ... b のアドレスのフラグが false になる

# b が使っていた場所が空いているので、そこが使われる
assign_int(:d, 44)

変数を使ってリストを作成。

# c1, c2, c3 を assign
assign_cons(:c1, name_to_addr(:a), NIL_ADDR)
assign_cons(:c2, name_to_addr(:d), name_to_addr(:c1))
assign_cons(:c3, name_to_addr(:c), name_to_addr(:c2))

# 変数の場合と同様に unbind/sweep して c3 をなかったことに
unbind(:c3)
sweep()

# c3 が使っていた場所が空いているので、そこが使われる
assign_cons(:c4, name_to_addr(:c), name_to_addr(:c2))

この状態でメモリと使用状況をダンプするとこう(* が付いているものが使用中):

   0 * nil
   1 * 11 ... a
   2 * 44 ... d
   3 * 33 ... c
   4 * 1  ... c1 の car => a のアドレス
   5 * 0  ... c1 の cdr => nil
   6 * 2  ... c2 の car => d のアドレス
   7 * 4  ... c2 の cdr => c1 の先頭アドレス
   8 * 3  ... c4 の car => c のアドレス
   9 * 6  ... c4 の car => c2 の先頭アドレス
  10   nil
  11   nil
  12   nil
  13   nil

c4 からリストをたどってみる:

def walk_list(addr)
  puts "--------"

  if addr == NIL_ADDR
    puts "リストの終端に達した"
    return
  end

  car_pos = addr
  cdr_pos = addr + 1
  puts "car の位置: #{car_pos}"
  puts "cdr の位置: #{cdr_pos}"

  car_content = $mem[car_pos]
  cdr_content = $mem[cdr_pos]
  puts "car の内容(参照先アドレス): #{car_content}"
  puts "cdr の内容(参照先アドレス): #{cdr_content}"

  # car の内容(参照先アドレス)を使った操作
  puts "★ car: #{ $mem[car_content] }"

  # cdr の内容(参照先アドレス)を使った操作
  walk_list(cdr_content)
end

walk_list(name_to_addr(:c4))

結果:

--------
car の位置: (Addr 8)
cdr の位置: (Addr 9)
car の内容(参照先アドレス): (Addr 3)
cdr の内容(参照先アドレス): (Addr 6)
★ car: 33
--------
car の位置: (Addr 6)
cdr の位置: (Addr 7)
car の内容(参照先アドレス): (Addr 2)
cdr の内容(参照先アドレス): (Addr 4)
★ car: 44
--------
car の位置: (Addr 4)
cdr の位置: (Addr 5)
car の内容(参照先アドレス): (Addr 1)
cdr の内容(参照先アドレス): (Addr 0)
★ car: 11
--------
リストの終端に達した

動きますね。


ためしに、unbind のたびに sweep するのをやめて最後に1回だけ sweep を実行した場合:

   0 * nil
   1 * 11
   2   22
   3 * 33
   4 * 44
   5 * 1
   6 * 0
   7 * 4
   8 * 5
   9   3
  10   7
  11 * 3
  12 * 7
  13   nil

歯抜けになりました(これはこれで期待通り)。


ふーむ……こんなんでいいんでしょうか。

consごっこ



表面だけ見ると Ruby の Array、 Hash っぽいけど中身は cons セル、というものを書いてみました。

試してみたくなって、なんとなく書いてみた、という感じのものです。 cons セルさえあれば基本的なデータ構造が作れてなんとかなるんだな、という感触がふんわり得られればOK。 なので、効率の悪さは無視します。

あまり調べずに適当に書いたので、一般的な LispScheme とは命名や動作が微妙に違っている気がします。


まず ConsCell クラスと cons メソッド。

class ConsCell
  attr_reader :car, :cdr

  def initialize(first, rest)
    @car = first
    @cdr = rest
  end

  def _to_s(x)
    x.nil? ? "nil" : x.to_s
  end

  def to_s
    "(#{ _to_s(@car) } . #{ _to_s(@cdr) })"
  end
end

def cons(first, rest)
  ConsCell.new(first, rest)
end
puts cons(1, nil)        #=> (1 . nil)
puts cons(1, 2)          #=> (1 . 2)
puts cons(cons(1, 2), 3) #=> ((1 . 2) . 3)

それから car, cdr, list メソッド。

def car(cell)
  cell.car
end

def cdr(cell)
  cell.cdr
end

def list(*args)
  return nil if args.empty?
  first, *rest = args
  cons(first, list(*rest))
end
puts car(cons(1, 2))          #=> 1
puts cdr(cons(1, 2))          #=> 2
puts cdr(cons(1, cons(2, 3))) #=> (2 . 3)
puts list(1, 2, 3)            #=> (1 . (2 . (3 . nil)))

リスト操作処理をいくつか。

def list_nth(xs, i)
  return car(xs) if i == 0
  list_nth(cdr(xs), i - 1)
end

def list_size(xs, n = 0)
  return n if xs.nil?
  list_size(cdr(xs), n + 1)
end

def list_append(xs, x)
  return cons(x, nil) if xs.nil?

  first = car(xs)
  rest = cdr(xs)
  cons(
    first,
    list_append(rest, x)
  )
end

Ruby の Array っぽく使えるクラスを作ってみます。 起点となる cons セルを @cell に保持しておいて、あとはさっき作った操作用メソッドを呼ぶだけ。

中身は Lisp でガワだけオブジェクト指向っぽく使えるようにした、みたいな感じでしょうか。

class ConsArray
  def initialize(*args)
    @cell = list(*args)
  end

  def size
    list_size(@cell)
  end

  def [](i)
    list_nth(@cell, i)
  end

  def <<(x)
    @cell = list_append(@cell, x)
  end

  def to_s
    @cell.to_s
  end
end

それっぽく動くことがふんわり分かればよいだけなので、とりあえず size [] << だけ作ってみました。


同様に、Ruby の Hash っぽく使えるクラスも作ってみました。 ConsHash という名前が微妙ですね。

def alist_create(args)
  return nil if args.nil?

  first  = car(args)
  second = car(cdr(args))
  rest   = cdr(cdr(args))

  cons(
    cons(first, second),
    alist_create(rest)
  )
end

def alist_get(alist, key)
  return nil if alist.nil?

  pair = car(alist)
  if car(pair) == key
    cdr(pair)
  else
    alist_get(cdr(alist), key)
  end
end

def alist_key?(alist, key)
  return false if alist.nil?

  pair = car(alist)
  if car(pair) == key
    true
  else
    alist_key?(cdr(alist), key)
  end
end

def alist_delete(alist, key)
  return if alist.nil?

  pair = car(alist)
  if car(pair) == key
    cdr(alist)
  else
    cons(
      pair,
      alist_delete(cdr(alist), key)
    )
  end
end

def alist_set(alist, key, val)
  cons(
    cons(key, val),
    alist_delete(alist, key)
  )
end

class ConsHash
  def initialize(*pairs)
    args = pairs.flatten
    @cell = alist_create(list(*args))
  end

  def key?(key)
    alist_key?(@cell, key)
  end

  def [](key)
    alist_get(@cell, key)
  end

  def []=(key, val)
    @cell = alist_set(@cell, key, val)
  end

  def delete(key)
    @cell = alist_delete(@cell, key)
  end

  def to_s
    @cell.to_s
  end
end

Ubuntu 18.04にJupyter NotebookとIRubyをインストール(pyenv, rbenv を使用)

(2022-05-07 追記) Ubuntu 22.04 版を書きました memo88.hatenablog.com



バージョンなど

Ubuntu 18.04
anyenv
pyenv
  Python 3.7.7
rbenv
  Ruby 2.7.1

jupyter 1.0.0
iruby 0.4.0

Docker の用意

Docker を使っているのはまっさらな状態に戻してやり直したりしたかったためです。 Docker なしでも大体同じだと思います。

# Dockerfile

FROM ubuntu:18.04

RUN apt-get update \
  && apt-get install -y sudo git wget build-essential nano

RUN useradd --create-home --gid sudo --shell /bin/bash user1 \
  && echo 'user1:pass' | chpasswd \
  && echo 'Defaults visiblepw'           >> /etc/sudoers \
  && echo 'user1 ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

USER user1

WORKDIR /home/user1

CMD ["/bin/bash"]

参考: Dockerコンテナ内にsudoユーザを追加する - Qiita

イメージをビルドしてコンテナを起動。

docker build -t ubuntu_jupyter:trial .
docker run --rm -it -p8888:8888 ubuntu_jupyter:trial bash

以下はコンテナ内で作業しています。

anyenv, rbenv, pyenv のインストール

git clone https://github.com/anyenv/anyenv ~/.anyenv

export PATH="$HOME/.anyenv/bin:$PATH"
echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(anyenv init -)"'               >> ~/.bashrc

exec bash -l

## メッセージにしたがって実行
yes | anyenv install --init

anyenv install rbenv
anyenv install pyenv
exec bash -l

Ruby 2.7.1 のインストール

sudo apt install -y libssl-dev zlib1g-dev
rbenv install 2.7.1

Docker のポートマッピングの確認。 先に疎通確認しておきます。

RBENV_VERSION=2.7.1 ruby -run -e httpd -- --port=8888 --bind-address=0.0.0.0 .

ホスト側から http://localhost:8888/ にアクセスできることを確認して Ctrl-C で止める。

Python 3.7.7 のインストール

sudo apt install -y libffi-dev libsqlite3-dev

env PYTHON_CONFIGURE_OPTS='--enable-shared' pyenv install 3.7.7

env PYTHON_CONFIGURE_OPTS='--enable-shared' は後で PyCall を使うための指定
https://github.com/mrkn/pycall.rb#note-for-pyenv-users

Jupyter Notebook のインストール

# ディレクトリと rbenv, pyenv の用意

mkdir jupyter
cd jupyter/
pwd #=> /home/user1/jupyter

rbenv local 2.7.1
pyenv local 3.7.7

ruby -v   #=> ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
python -V #=> Python 3.7.7
## bundle init みたいなもの
## "venv.d" は任意のディレクトリ名
python -m venv venv.d

## 常に bundle exec してるみたいなモードになる
## モードを抜けたい場合は deactivate を実行する
. venv.d/bin/activate

## Gemfile に追加 + bundle install みたいなもの
pip install jupyter

jupyter notebook --no-browser --ip=0.0.0.0

## ログイン用のトークン付きのURLが表示されるので、ホスト側のブラウザで開く

--ip=0.0.0.0 を指定しているのは Docker コンテナ内で実行してホスト側から参照するため。

確認できたら Ctrl-C で止める。

IRuby のインストール

https://github.com/SciRuby/iruby
のインストールの説明を参考にしてインストール

sudo apt install -y libtool libffi-dev ruby ruby-dev make
sudo apt install -y libzmq3-dev libczmq-dev

bundle init

echo 'gem "ffi-rzmq"' >> Gemfile
echo 'gem "iruby"'    >> Gemfile
echo 'gem "pycall"'   >> Gemfile

bundle install --path=vendor/bundle

## こういうメッセージが出る。 pry とかあると便利?
## Consider installing the optional dependencies to get additional functionality:
##   * pry
##   * pry-doc
##   * awesome_print
##   * gnuplot
##   * rubyvis
##   * nyaplot
##   * cztop
##   * rbczmq

bundle exec iruby register --force
## ~/.local/share/jupyter/kernels/ruby/kernel.json
## が生成される

cat ~/.local/share/jupyter/kernels/ruby/kernel.json
#=> {"argv":["/home/user/jupyter/vendor/bundle/ruby/2.7.0/bin/iruby","kernel","{connection_file}"],"display_name":"Ruby 2.7.1","language":"ruby"}

## この状態で jupyter 起動
jupyter notebook --no-browser --ip=0.0.0.0
## → Ruby 2.7.1 のノートブックが新規作成できるようになる

しかし、次のようなエラーが jupyter を起動したターミナルに出て うまく動かない。

[I 07:06:44.327 NotebookApp] KernelRestarter: restarting kernel (3/5), new random ports
Traceback (most recent call last):
        2: from /home/user/jupyter/vendor/bundle/ruby/2.7.0/bin/iruby:23:in `<main>'
        1: from /home/user/.anyenv/envs/rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems.rb:294:in `activate_bin_path'
/home/user/.anyenv/envs/rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems.rb:275:in `find_spec_for_exe': can't find gem iruby (>= 0.a) with executable iruby (Gem::GemNotFoundException)

これはおそらく rbenv + bundler 環境で実行できていないせいなので、 iruby コマンドのラッパー iruby.sh を用意して対処してみる。

(他に良い方法があるかもしれませんが、とりあえずこれで動きました。)

cat <<'EOB' > iruby.sh
#!/bin/bash

JUPYTER_DIR=~/jupyter

export PYENV_ROOT="${HOME}/.anyenv/envs/pyenv"
export LIBPYTHON=${PYENV_ROOT}/versions/3.7.7/lib/libpython3.7m.so.1.0
export PYTHON=${JUPYTER_DIR}/venv.d/bin/python
# これでもいい?
# export PYTHON=${PYENV_ROOT}/shims/python

export RBENV_ROOT="${HOME}/.anyenv/envs/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"

rbenv shell 2.7.1

BUNDLE_GEMFILE=${JUPYTER_DIR}/Gemfile \
  bundle exec iruby "$@"
EOB
## 実行権限付ける
chmod u+x iruby.sh

## iruby のパスを修正
nano ~/.local/share/jupyter/kernels/ruby/kernel.json

{
  "argv":[
    "/home/user1/jupyter/iruby.sh",  ...ここだけ修正
    "kernel",
    "{connection_file}"
  ],
  "display_name":"Ruby 2.7.1",
  "language":"ruby"
}

## もう一度 jupyter を起動
jupyter notebook --no-browser --ip=0.0.0.0

これで ruby カーネルが動くようになった。

基本的な使い方については IRuby Notebook 利用者ガイド が参考になります。

ライブラリの追加

Python のライブラリを追加したい場合

pip install foo_lib

カーネルを再起動(ノートブックのページのメニューの Kernal → Restart) すると import できるようになる。

Ruby のライブラリを追加したい場合

echo 'gem "lib_foo"' >> Gemfile
bundle install

カーネルを再起動すると require できるようになる。

参考