Qiita の方に書きました。
Ruby/Racc: パースに失敗した位置(行、桁)を得る
Qiita の方に書きました。
きしださんのかわいいリレーショナルデータベースをRubyで写経した
きしださんのかわいいリレーショナルデータベースの最初のバージョンを写経してみました。 この記事、もう8年前なんですね。ついこないだ読んだような気がしていましたが……。
簡単なものだったら自作できないかなと以前から思っていたんですよね。 最近やっと重い腰を上げて着手したのですが、 そこできしださんの記事のことをふと思い出し、 参考のために写経してみました。 そういえば、そもそもリレーショナルデータベース自作について考えるようになったきっかけの1つもきしださんの記事だった気がします。
一度そのまま書いたあと、いろいろいじってみました。 それにより非効率になっている部分もありますが、ひとまず自分にとっての理解のしやすさを優先しました。
以下の kawaii_rdb.rb
と test_kawaii_rdb.rb
を同じディレクトリに置いて
ruby test_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
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
参考
- (2017-02-11) Apache ArrowのRubyバインディングをGObject Introspectionで - Kouhei Sutou - Rabbit Slide Show
- p42〜47 array の作りかた
- Apache Arrow GLib Reference Manual: Apache Arrow GLib Reference Manual
- C/GLib 版のリファレンス
- Tabular Data — Apache Arrow v1.0.0
- C++ 版のドキュメント
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。 なので、効率の悪さは無視します。
あまり調べずに適当に書いたので、一般的な Lisp や Scheme とは命名や動作が微妙に違っている気がします。
まず 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
正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した
Qiita に引っ越しました。
四則演算と剰余のみのexprコマンドをRubyで作ってみた
Zenn に引っ越しました。
Ubuntu 18.04にJupyter NotebookとIRubyをインストール(pyenv, rbenv を使用)
(2022-05-07 追記) Ubuntu 22.04 版を書きました memo88.hatenablog.com
- バージョンなど
- Docker の用意
- anyenv, rbenv, pyenv のインストール
- Ruby 2.7.1 のインストール
- Python 3.7.7 のインストール
- Jupyter Notebook のインストール
- IRuby のインストール
- ライブラリの追加
- 参考
バージョンなど
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
基本的な使い方については IRuby Notebook 利用者ガイド が参考になります。
ライブラリの追加
Python のライブラリを追加したい場合
pip install foo_lib
カーネルを再起動(ノートブックのページのメニューの Kernal → Restart) すると import できるようになる。
Ruby のライブラリを追加したい場合
echo 'gem "lib_foo"' >> Gemfile bundle install
カーネルを再起動すると require できるようになる。