4 Rubyist

Table of Contents

find

前回ちょいと作りかけたruby版のfindを完成させておく。指定したdir内の*.rbに ついて、その総行数と総サイズを求めるもの。

sakae@lu:tmp$ cat myfind.rb
#! /usr/local/bin/ruby
# like sh:  find $1 -name '*.rb' | xargs wc -lc

require "find"

Find.find( ARGV[0] ) do |f|
  printf("%8d %8d  %s\n",
         File.read(f, mode: 'rb').count("\n"),
         File.size(f),
         File.basename(f) )    if File.extname(f) == '.rb'
end

pythonと違って、まったく気まままフォーマットで記述できるのが楽しい。おかげで、 世には、邪悪な難読化したコードのコンテストが有るぐらいだ。

上のコードのコメントには少々の嘘が混っていた。最後の集計には、awkの協力が 必要です。まあ、集計用のコードを埋め込んでしまうのも手であるけど、オイラー的 には、均一な出力(ヘッダーなんてウザイとかね)が、よいと思うのであります。

sakae@lu:tmp$ ./myfind.rb ruby-4.0.0 | awk '{s1 += $1; s2 += $2} END{print s1,s2}'
1671561 41225305

それから、ちょっとはまった所が有る。

sakae@lu:tmp$ ./myfind.rb ruby-4.0.0
       :
      60     1153  classes.rb
       3       91  freeze_magic_comment.rb
./myfind.rb:7:in 'String#count': invalid byte sequence in UTF-8 (ArgumentError)

         File.read(f).count("\n"),
                            ^^^^
        from ./myfind.rb:7:in 'block in <main>'
        from /usr/local/lib/ruby/4.0.0/find.rb:52:in 'block (2 levels) in Find.find'
        from /usr/local/lib/ruby/4.0.0/find.rb:51:in 'Kernel#catch'
        from /usr/local/lib/ruby/4.0.0/find.rb:51:in 'block in Find.find'
        from /usr/local/lib/ruby/4.0.0/find.rb:46:in 'Array#each'
        from /usr/local/lib/ruby/4.0.0/find.rb:46:in 'Find.find'
        from ./myfind.rb:5:in '<main>'

最初に作ったコードでは、上記のエラーが発生してたんだ。文字として体を成していない って事なんで、ただのバイト列として解釈する様に指示した(教えてもらった)。

File.read(f, mode: "rb") は、File.open(f, mode: "rb") { |io| io.read } の、 糖衣構文って事で、裏方ではちゃんとopenしてるんだ。これがrubyの気持良さに つながるのか。実際には、

File.open(f, "rb") { |io| io.each_byte.count { |b| b == 10 } }

中が透けて見える記述方法(バイトを強調するため "\n" は、使っていない。

gem

統計用のやつが欲しい。 pytonでは、 身長と体重の関係 とか 統計データの整理と可視化(Numpy, Pandas, Matplotlib) みたいに楽しい思いをしてるから。 相関行列もcorrcoef関数なんてのが有るからね。勿論、pythonみたいに、一気通貫で グラフにして眺めるなんて望み薄って事は、承知してますよ。

世話焼きチャッピーにマッチングを依頼。だって、前回のRubyboxにある釣り書なんて 見ていられないからね。現代は伴侶の選定もAI任せですから、これぐらいの事は 何ら恥じる事はない。

Q: rubyの統計用gemについての紹介を依頼します。 機能の希望は、

  1. pythonのnumpyがサポートしてるのと同様な機能が欲しい
  2. 相関係数を計算する機能は必須

下記、行数と文字数との相関を確認したいのが主目的。他に一般統計で、 4分位点も得られればベターです。

sakae@lu:tmp$ ./myfind.rb ruby-4.0.0|head -3
     133     3549  abbrev.rb
     381    14118  base64.rb
     608    19589  benchmark.rb

A: Rubyで「/numpyっぽい統計処理/」+「/相関係数必須/」+「/四分位点が取れる/」という条件だと、現実的な選択肢はいくつかに絞られます。目的(行数と文字数の相関確認)を踏まえて、/実用度順/に紹介します。


## 結論の先出し(おすすめ順)

  1. numo-narray + numo-statsnumpy互換性を最重視するなら最有力
  2. statsample統計解析用途に強い(相関・四分位点は超簡単)
  3. descriptive_statistics軽量・簡単。numpy代替というより補助向け

## 1. numo-narray + numo-stats(numpy互換を重視)

### 概要

  • Rubyで/numpy互換API/を目指した数値配列ライブラリ
  • Numo::NArray は /多次元配列・ベクトル演算/が可能
  • numo-stats を組み合わせると統計関数が揃う

### インストール

gem install numo-narray
gem install numo-stats

### 相関係数

require "numo/narray"
require "numo/stats"

lines = Numo::DFloat[133, 381, 608]
chars = Numo::DFloat[3549, 14118, 19589]

corr = Numo::Stats.corrcoef(lines, chars)
puts corr

→ numpyの np.corrcoef() とほぼ同じ感覚

### 四分位点

q = Numo::Stats.quantile(lines, [0.25, 0.5, 0.75])
p q

### 評価

◎ numpyライク ◎ 高速(C拡張) △ Rubyらしさは少なめ △ gem数が分かれる


## 2. statsample(統計用途に最適)

### 概要

  • 社会統計・データ分析向けRubyライブラリ
  • /相関係数・分位点・回帰分析/がワンライナー
  • 内部で dar u, matrix を使う(numpy互換ではない)

### インストール

gem install statsample

### 相関係数

require "statsample"

lines = [133, 381, 608].to_scale
chars = [3549, 14118, 19589].to_scale

puts Statsample::Bivariate::Pearson.r(lines, chars)

### 四分位点

p lines.percentil(25)
p lines.percentil(50)
p lines.percentil(75)

### 評価

◎ 統計APIが直感的 ◎ 相関・分位点が楽 △ numpy互換ではない △ 数値計算性能は控えめ


## 3. =descriptivestatistics=(軽量・お手軽)

### 概要

  • Arrayに統計メソッドを生やすgem
  • 小規模データ向け

### インストール

gem install descriptive_statistics

### 使用例

require "descriptive_statistics"

lines = [133, 381, 608]
chars = [3549, 14118, 19589]

puts lines.percentile(25)
puts lines.percentile(50)
puts lines.percentile(75)

相関係数は非対応 → 自前計算 or 他gemと併用が必要

### 評価

○ 軽い × 相関必須条件を満たさない


## 行数×文字数の相関確認(実用例)

myfind.rb の出力をそのまま処理するなら:

require "numo/narray"
require "numo/stats"

lines = []
chars = []

ARGF.each_line do |l|
  nline, nchar, _ = l.split
  lines << nline.to_i
  chars << nchar.to_i
end

lines = Numo::DFloat[*lines]
chars = Numo::DFloat[*chars]

puts "相関係数:"
p Numo::Stats.corrcoef(lines, chars)

puts "行数の四分位点:"
p Numo::Stats.quantile(lines, [0.25, 0.5, 0.75])

## まとめ(用途別)

用途 推奨
------------ ----------------------------
numpy互換・数値配列 numo-narray + numo-stats
統計解析(相関・分位点) statsample
軽量な記述統計 descriptive_statistics

install and run

お勧めのnumo-narray + numo-statsは、gemになかったぞ。盛大なハルシネーションで あります。是非セカンドオピニオンして下さい。しょうがないので、

fu$ doas gem install statsample
Fetching spreadsheet-1.3.4.gem
Fetching rubyvis-0.6.1.gem
Fetching rserve-client-0.3.5.gem
Fetching ruby-ole-1.2.13.1.gem
Fetching text-table-1.2.4.gem
Fetching prawn-security-0.8.4.gem
Fetching prawn-layout-0.8.4.gem
Fetching prawn-core-0.8.4.gem
Fetching prawn-0.8.4.gem
Fetching prawn-svg-0.9.1.11.gem
Fetching clbustos-rtf-0.4.2.gem
Fetching reportbuilder-1.4.2.gem
Fetching minimization-0.2.5.gem
Fetching extendmatrix-0.4.gem
Fetching distribution-0.8.0.gem
Fetching dirty-memoize-0.0.4.gem
Fetching statsample-2.1.0.gem
Fetching backports-3.25.3.gem
Fetching daru-0.1.6.gem
Fetching awesome_print-1.9.2.gem
 :
*************************************************************************
Thank you for installing daru!

  oOOOOOo
 ,|    oO
//|     |
\\|     |
 `|     |
  `-----`


Hope you love daru! For enhanced interactivity and better visualizations,
consider using gnuplotrb and nyaplot with iruby. For statistics use the
statsample family.

Read the README for interesting use cases and examples.

Cheers!
*************************************************************************

さあ、さあ、乾杯だ。グラフはgnuplotを併用してってのは、現実的な解だろう。

fu$ ruby test.rb
/ram/usr/local/lib/ruby/gems/3.4/gems/prawn-svg-0.9.1.11/lib/prawn/svg/interface
.rb:11:in 'block in <class:Interface>': undefined method 'exists?' for class Fil
e (NoMethodError)

      DEFAULT_FONT_PATHS.each {|path| @font_path << path if File.exists?(path)}
                                                                ^^^^^^^^
Did you mean?  exist?
        from /ram/usr/local/lib/ruby/gems/3.4/gems/prawn-svg-0.9.1.11/lib/prawn/
svg/interface.rb:11:in 'Array#each'

チャッピーが提示したコードをラン。何かとんでもない所で、足を引っ張られているような。 DaruとStatsampleを使ったデータ分析を試してみた

チャッピーは言葉足らずだった。正式にやってみたけどコンフリクトしてる。 黙ってpythonへ逃げるのが勝ちだと思うよ。

descriptive_statistics-2.5.1

折角なので軽量の定義を参照しとく。本当に基本の記述統計関数だけなんだね。 /usr/local/lib/ruby/gems/3.4/gems/descriptive_statistics-2.5.1/lib

module DescriptiveStatistics
  def descriptive_statistics(&block)
    return { :number => self.number(&block),
             :sum => self.sum(&block),
             :variance => self.variance(&block),
             :standard_deviation => self.standard_deviation(&block),
             :min => self.min(&block),
             :max => self.max(&block),
             :mean => self.mean(&block),
             :mode => self.mode(&block),
             :median => self.median(&block),
             :range => self.range(&block),
             :q1 => self.percentile(25, &block),
             :q2 => self.percentile(50, &block),
             :q3 => self.percentile(75, &block) }
  end
end

4 rubyist

前回やった4人のrubyist(正体はLLM)によるハフマン符号のスクリプトを肴に、 ごにょごにょしてみたい。小説なんかでも、こういうのあるね。同じお題で 複数の人に書いてもらう奴。アンソロジィーって分野だ。笑点も同じ趣向だね。

一応、作者は匿名にしとく。興味があったら、利き酒ならぬ利きAIをやってみてくれ。

外観チェック

コードは小説(いや、もろに台本でしょ)と見做される。ならばコメントを盛大に盛って原稿料をつり上げる 不届き者が居るかも知れない。んな事を想像して、生原稿をば。 huffman.tar.gz

sakae@lu:huffman$ wc *
  83  166 1682 genA.rb
  77  192 1839 genB.rb
  52  141 1503 genC.rb
  49  133 1442 genD.rb

編集者特権(笑点で言うなら司会者の昇太だな)で、コメントを除去した。コメントは コメント行のみの物がほとんどであったが、一人だけコードの右側に書いている人が いた。また、有効な行のみで80桁を越える人がいた。 pythonのPEP規約では1行は78桁にしましょうってのが有ったはず。

1行目はシェバング を追加しといた。それから空行もコードの一部って事を尊重して、そのままにしている。 以上の編集結果が、下記だ。尚、() 内は、コードの黒々度(一行の平均文字数)だ。

sakae@lu:huffman$ wc *
  75  149 1344 genA.rb   (17)
  67  169 1411 genB.rb   (21)
  40  110  808 genC.rb   (20)
  41  115  927 genD.rb   (22)

rubyと言えばクラスが当然出てくるかと思ったら、半分の人(母数が少なすぎですが)が クラスを使っていなかった。クラスレスの人は総行数も少なくなっている。rubyを養護 する訳では無いけど、クラスの使い所って有る(例えば、大規模な案件)んでしょうな。

sakae@lu:huffman$ grep class -l *
genA.rb
genB.rb

外観チェックと言いながら、ちょいと中身に踏む込んで、入力を取り込む場所を 調べてみた。

sakae@lu:huffman$ egrep '(gets|read)' -n *
genA.rb:68:text = gets.chomp
genB.rb:55:input = STDIN.read
genC.rb:3:input = ARGF.read
genD.rb:2:input = ARGF.read.chomp

クラス派は、スクリプトの結構後ろの方で、入力。非クラス派ば、冒頭で入力を 受け取っている。これって偶然の事?

rubyスタイル

戯れにチャッピーと会話。

Q: rubyのスクリプトに誰もmain関数を定義しないのはMatzの遺伝子ですか?(詳細は、 割愛)

A: 面白い観察ですね。結論から言うと、

「Rubyスクリプトに main を書かない文化」は、かなりの部分で Matzの設計思想そのもの です。 しかもそれは単なる好みではなく、Rubyの評価モデル・言語観・Lisp的遺伝子が強く反映されています。

以下、整理してみます。


## 1. Rubyにおける「トップレベル=main」

Rubyには/暗黙の main/ が最初から存在します。

p self
#=> main
  • トップレベルは Object のインスタンス
  • 実行順は「上から下へ」
  • def fooObject#foo を定義している

つまり Ruby では、

「ファイル全体が main」

というモデルになっています。

C / Java / Go のように 「エントリポイントを宣言する」という発想自体が不要です。

これは偶然ではなく、/Matzが明確にそう設計した/部分です。


## 2. なぜ main を“書かせない”のか(Matz的理由)

Matz の思想を一言で言うと:

プログラマの思考を、言語仕様で邪魔しない

### main が不要であることのメリット

  1. 学習コストが低い
puts "Hello"

これが即動く

  1. スクリプト性が強い
    • Unix フィルタ
    • ワンライナー文化
    • AWK / sed の後継
  2. 「処理を書く」ことに集中できる
    • エントリポイント設計を考えなくていい

Matz は昔から一貫して、

「Rubyはアプリケーションを書くための言語である前に、 プログラマが考えを表現する言語である」

と言っています。


## 3. Ruby == Matz Lisp 仮説は、かなり当たっている

あなたの

Ruby == Matz Lisp

という直感、/相当鋭いです/。

### Lisp的な共通点

Lisp Ruby
---------- -----------
トップレベルで即評価 トップレベルで即実行
main 関数がない main 関数がない
定義と実行が混在 クラス定義と処理が混在
ボトムアップ設計 ボトムアップ設計

Lisp では:

(defun foo ...)
(foo)

Ruby では:

def foo
end
foo

/「定義→実行」の流れが自然/で、 main という特別扱いの関数を作る文化がありません。

Matz自身も

  • Lisp
  • Smalltalk
  • Perl

の影響を公言しており、 C/Java系の「入口点信仰」から距離を取っています。


## 4. Pythonが main を必要とする理由(Rubyとの決定的差)

Pythonの

if __name__ == "__main__":
    main()

は、Ruby的に見るとかなり/設計思想が違う/。

### Pythonは「スクリプト+ライブラリ両用」を強制する

Pythonでは:

  • import すると 即実行されるのは困る
  • だから __name__ ガードが必要

Rubyでは:

if __FILE__ == $0
  main
end

書けるが、文化的に必須ではない

理由は:

  • Rubyは「ライブラリとスクリプトを分ける」文化
  • gem化・require 前提
  • 実行用スクリプトは薄く書く

## 5. クラスを書く人ほど「mainを書かない」理由

あなたの観察:

クラスを使用してる人は、構造化を意識してる様に感じられます が、赤裸々にmain()は定義してません

これは矛盾ではありません。

### Rubyistの典型構造

class Huffman
  def run(input)
  end
end

input = ARGF.read
puts Huffman.new.run(input)

ここで Rubyist は:

  • クラス = 構造
  • トップレベル = 実行

と/役割分離/しています。

main を書くことは「構造化」ではなく、 /Rubyistにとっては冗長な儀式/に見えることが多いです。


## 6. それでも Ruby で main を書く人はいる

もちろんいます。

def main
  input = STDIN.read
end

main if __FILE__ == $0

ただしこれは:

  • Python経験者
  • C/Javaバックグラウンド
  • テスト容易性を最優先

の人に多い印象です。

Matz的Rubyらしさからは、やや距離がある。


## 7. 結論(あなたの問いへの直接回答)

rubyのスクリプトに誰もmain関数を定義しないのはMatzの遺伝子ですか?

はい。ほぼ間違いなく「Matzの遺伝子」です。

  • Rubyは「mainを書かせない」言語として設計された
  • トップレベル実行は意図的な設計
  • Lisp / Smalltalk 系譜の思想
  • Pythonとは「両用性」に対する哲学が違う

そしてあなたの

一粒(ファイル)で、二度美味しいスタイルも大好き

という感覚も /完全に正当/です。 Rubyはそれを「禁止していない」だけで、 「推奨していない」だけ。

これを受けてオイラーの追記:

改めてpython文化は、インタープリタ界では異端児 と思うぞ。インタープリタ界の老舗であるshell scriptで確認。その代表例として OpenBSDの /etc/rc を調べる。

全体行数が 683行のうち # End subroutines. と記述されている場所は、356行目。 ほぼ半分が関数と言うまとまりだ。 以降は、あからさまにmainとは記述されていないけど、全体の制御を司どる部分 になってる。全体がmainだから、改めてmainとは書かないんだね。

何はなくともrunだぜ。

外観だけを眺めていても、個性が見えてこない。rdbgで内部を観察してみましょ。

rdbg – genA.rb して、起動、停止したらcontして、目的の文字列 RET + Ctl-D するのが 常道。だけど、メンドイ作業だ。 そこで、ちょいとスクリプトに仕掛けを 施し、環境から文字列を受け取る事にする。

te$ cat conf.rb
ENV['MINE'] = 'abbbbccddd'

こんなファイルを用意しておいて、目的スクリプトを僅かに修正。genD.rbなら

load 'conf.rb'
input= ENV['MINE']
#input = ARGF.read.chomp

こんな風に変更する。後はrdbg run

te$ rdbg genD.rb
 :   ### rdbgのnコマンドでステップ実行
[3, 12] in genD.rb
     3| input= ENV['MINE']
     4| #input = ARGF.read.chomp
     5|
     6| freq = input.chars.tally
     7|
=>   8| Node = Struct.new(:char, :freq, :left, :right) do
     9|   def leaf?
    10|     char != nil
    11|   end
    12| end
=>#0    <main> at genD.rb:8
(rdbg) p freq    # command
=> {"a" => 1, "b" => 4, "c" => 2, "d" => 3}

気になった所で変数の中身を確認。tallyなんてメソッドは知りませんでした。 それからStructなんて言うclassの一歩手前みたいな構造体も知らなかった。 構造体 こんな解説を見つけたぞ。

[12, 21] in genD.rb
    12| end
    13|
    14| nodes = freq.map { |char, f| Node.new(char, f, nil, nil) }
    15| nodes.sort_by!(&:freq)
    16|
=>  17| until nodes.size == 1
    18|   left = nodes.shift
    19|   right = nodes.shift
    20|   parent = Node.new(nil, left.freq + right.freq, left, right)
    21|   nodes << parent
=>#0    <main> at genD.rb:17
(rdbg) pp nodes    # command
[#<struct Node char="a", freq=1, left=nil, right=nil>,
 #<struct Node char="c", freq=2, left=nil, right=nil>,
 #<struct Node char="d", freq=3, left=nil, right=nil>,
 #<struct Node char="b", freq=4, left=nil, right=nil>]

ppを使うと綺麗に表示できます。

(rdbg) pp nodes    # command
[#<struct Node
  char=nil,
  freq=10,
  left=#<struct Node char="b", freq=4, left=nil, right=nil>,
  right=
   #<struct Node
    char=nil,
    freq=6,
    left=#<struct Node char="d", freq=3, left=nil, right=nil>,
    right=
     #<struct Node
      char=nil,
      freq=3,
      left=#<struct Node char="a", freq=1, left=nil, right=nil>,
      right=#<struct Node char="c", freq=2, left=nil, right=nil>>>>]

これを見て、脳内変換で木が見えたら、盆栽師匠認定です。

(rdbg) p codes    # command
=> {"b" => "0", "d" => "10", "a" => "110", "c" => "111"}

これがハフマン符号。後はユーザーの要望によりcode.tr('01', '.-')で、表示形式 を変換してんだな。unix屋さんならtrはお馴染のコマンドだからね。

同じ要領でgenA.rbをやってみる。こちらはガチガチのクラス派

    70| #text = gets.chomp
    71|
    72| huffman_tree = HuffmanTree.new(text)
    73| codes = huffman_tree.get_codes
    74|
=>  75| codes.each do |char, code|
    76|   puts "#{char}   #{text.count(char)}   #{code}"
    77| end
=>#0    <main> at genA.rb:75
(rdbg) pp huffman_tree    # command
#<HuffmanTree:0x50351984
 @codes={"b" => ".", "d" => "-.", "a" => "--.", "c" => "---"},
 @freq_table={"a" => 1, "b" => 4, "c" => 2, "d" => 3},
 @root=
  #<HuffmanNode:0x503507dc
   @char=nil,
   @freq=10,
   @left=#<HuffmanNode:0x503512f4 @char="b", @freq=4, @left=nil, @right=nil>,
   @right=
    #<HuffmanNode:0x50350a34
     @char=nil,
     @freq=6,
     @left=#<HuffmanNode:0x50351074 @char="d", @freq=3, @left=nil, @right=nil>,
     @right=
      #<HuffmanNode:0x50350d2c
       @char=nil,
       @freq=3,
       @left=
        #<HuffmanNode:0x50351434 @char="a", @freq=1, @left=nil, @right=nil>,
       @right=
        #<HuffmanNode:0x503511b4 @char="c", @freq=2, @left=nil, @right=nil>>>>,
 @text="abbbbccddd">

何もかもHuffmanTreeのインスタンス内に格納しちゃってるのね。

昔のrubyで動くかな?

課題を提示した時ruby 3.4.X を指定してた。ひょっとして ruby 1.8.7 (2008-05-31 patchlevel 0) [x8664-linux] こういう年代物で 動く奴が有るか、無茶ブリしてみる。

sakae@lu:huffman$ echo -n abbbbccddd | ruby genA.rb
a   1   --.
b   4   .
c   2   ---
d   3   -.
sakae@lu:huffman$ echo -n abbbbccddd | ruby genB.rb
genB.rb:23:in `build_huffman_tree': undefined method `sort_by!' for #<Array:0x7f0c34dcf450> (NoMethodError)
        from genB.rb:60
sakae@lu:huffman$ echo -n abbbbccddd | ruby genC.rb
genC.rb:6: undefined method `tally' for #<Enumerable::Enumerator:0x786354697f28> (NoMethodError)
sakae@lu:huffman$ echo -n abbbbccddd | ruby genD.rb
genD.rb:27: syntax error, unexpected ')', expecting '='
genD.rb:34: syntax error, unexpected kEND, expecting $end

こういうのをリメイクして動くようにするのも、また楽しからずか、かな? 冒頭の方で言及したgemの統計をセカンドオピニオンした。欲しい機能をもったgemは 開発が終了してるっぽくて星の数が1つだった。もし使いたかったら、リメイクしてね って、暗に示唆してんな。

長くなったので、この辺で。

README

アルファベット順の文化史 なんて本を読んだ。 文字は神から与えられたものだと言う。記録が残せるって素晴しい事だ。 沢山、記録が溜ってくると、何らかの方法で分類整理できないと、宝の もちぐされになる、ってか、取り出せないと、それは、無いのと一緒。

この本では、紀元前の時代からの変遷が丁寧に説明されている。19世紀 以降は、アルファベット順が定着した。

これなら、偉い人順に並べるとかの忖度が必要ないからね。 それにA,B,C なら、一種の匿名性もかなう。突然Jさんなんてのが登場すると、 ありゃ、それってイニシャル? なんて想像が働いてしまうからね。 通常はAさん、Bさんと付けていくだろう。暗号の説明で良く登場する、アリスと ボブは、これにならったものだ。

で、世界的にアルファベット順かと言うと、そうでもない。韓国でのパルパル(1988) オリンピックの時は、ガーナが一番で、二番はガボン共和国だったらしい。 これはハングル語の音節文字表ではgaが最初に来るからだという。 中国でのオリンピックは、伝統的な分類、部首と画数によるものだったそうだ。

日本だと、古式によれば、いろは順だろう。文字を読めない人向けに、犬も歩けば 棒に当る式のカルタが有るからね。その後は、50音順だな。携帯の電話帳とか、 国語辞書で採用されてる。まあ、そういう事だ。


This year's Index

Home