『プロを目指す人のための Ruby 入門』を読んでみた

 
評判がよいので読んでみました。アマゾンのレヴューで、独学で Ruby を勉強している人なら読むべきみたいなことも書いてありましたので。

結論からいうと、自分にはあまり得るところは多くなかったですね。プログラミング初心者では読めないでしょうが、本当に Ruby 初心者向きですね。Ruby を使いこなすにはリファレンス・マニュアル(日本語です)を読むのがいちばんですが、これが読める人なら本書は必要ないかも知れません。

けれども、本書がいい本でないというのではまったくありません。基本的に Ruby が使えるようになる(Ruby minimum)という目的のためなら、これはいい本だと思います。本書で弱いのは正規表現*1、ファイル処理*2クロージャ*3の説明くらいで、あとはじつに丁寧に書かれています。文章も読みやすいです。特に、アマゾンのレヴューにもありましたが、テストを積極的に書いて開発するという体裁になっていて、仕事に使う人には必須な気がします。まあ素人の僕などは最悪なことにテストなどまったく書かないので、こういうのを真似てはいけません。さすがに仕事で使っている人だなと思いました。

本書では最終的に Ruby on Rails を使うということが念頭におかれています。もっとも Rails に特化した本ではまったくないので、その点は Rails に関係ない人でも大丈夫です。ただ、これは本書とは関係ないですが、Ruby の人気はあまりにも RoR に負っているのですよね。いま出る Ruby 本は、Rails 関係のものが多いです。そもそも Ruby が世界的な人気言語になったのは RoR のおかげで、それゆえに RoR の退潮とともに Ruby 人気が陰ってきているのはまちがいありません。自分はこれは仕方のないことだと思いますが、Ruby というのがそれ自体非常に優れた、興味深い言語であることが忘れられがちなのは、Rubyist として残念な気がします。実際、Ruby 以降の言語で Ruby に何も影響を受けていないというような人気言語は少ないように思えます。まあそれは自分のような初心者の正確に判断できることではないですが。


話は替りますが、Ruby の入門書の定番といえばこれですよね。僕も愛用してきました。いまだに簡単なリファレンス本としてもよく参考にするくらいです。

たのしいRuby 第5版

たのしいRuby 第5版

これ、最新版は第五版なのですが、アマゾンのレヴューで非常に評価が悪い。自分は第四版で読んだのですが、たぶん中身はほとんど変っていないと推測します。なのに…。特に、「題に『たのしい』とあるが、全然楽しくない!」というレヴューが多くて、何なのだという感じ。Ruby が楽しい言語だというのが題の意図だと思うのですが…。しかし、これを読んでわからないとは。確かに、プログラミングを初めてやるという人のため本ではないですが、最良の Ruby 入門書として読み継がれてきたのですけれどね。読者の層が替わったということでしょうか。

あと Ruby の入門書としてはこれが有名です。

初めてのRuby

初めてのRuby

ただこれは他言語の使える人が最速で Ruby を習得するための本です。Ruby が初めてのプログラミング言語という人には向きません。

Ruby の特徴のひとつである「メタプログラミング」についてはこれ。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

わかりやすい本ですが、中身はやさしい本ではありません。入門書を読んでからでしょうね。


何だかまとまりのない記事になってしまった…。

*1:RubyPerl 由来で文字列処理が非常に強力なのも特徴のひとつなので、これは何かで補足しておくべきです。

*2:それにしても、ファイル処理の説明がないに等しいというのには驚きました。いまってファイル処理しないの? そんなことはないと思うのですが。

*3:クロージャはいまやたいていの人気言語でサポートされている重要なプログラミング機能です。クロージャが使えるとプログラミングの世界が大きく広がるので、是非身に付けたほうがよいと思います。よく説明は JavaScript でされています。もちろん Ruby でも使えます。Proc あるいは lambda がそれです。

無駄に複雑な関数型 FizzBuzz(Ruby)

trans = ->(i) {
  if i % 15 == 0
    "FizzBuzz"
  elsif i % 3 == 0
    "Fizz"
  elsif i % 5 == 0
    "Buzz"
  else
    i.to_s
  end
}

fizzbuzz = ->(n) {
  generate = ->(i, ar) {
    return ar if i > n
    generate[i + 1, ar + [trans[i]]]
  }
  generate[1, []]
}

p fizzbuzz[50]

何でも再帰で書きたくなる病…。ちなみに、逐字的に Lisp に移植できると思います。

※参考
FizzBuzz問題(Ruby) - Camera Obscura
みんな大好き FizzBuzz(Ruby, Python) - Camera Obscura

ふたたび Ruby で 8 queen(関数型プログラミングっぽく)

ここで Ruby で「8 クイーン問題」を解いてみましたが、関数型プログラミングのお勉強に Common Lisp のコード例を移植してみました。

参考にしたのはこのサイトです。

実行するとこんな感じです。

----------
No.1
...@....
.@......
......@.
..@.....
.....@..
.......@
....@...
@.......
----------
No.2
....@...
.@......
...@....
......@.
..@.....
.......@
.....@..
@.......
----------
...

全部で解は92通りあります。

コード。

class Array
  def car; first;   end
  def cdr; drop(1); end
end

attack = ->(x, xs) {
  attack_sub = ->(n, ys) {
    if ys.empty?
      true
    elsif ys.car + n == x or ys.car - n == x
      false
    else
      attack_sub[1 + n, ys.cdr]
    end
  }
  attack_sub[1, xs]
}

safe = ->(ls) {
  if ls.empty?
    true
  elsif attack[ls.car, ls.cdr]
    safe[ls.cdr]
  else
    false
  end
}

counter = 0

print = ->(board) {
  puts "-" * 10
  puts "No.#{counter += 1}"
  board.each do |po|
    st = "." * board.size
    st[po - 1] = "@"
    puts st
  end
}

queen = ->(nums, board) {
  if nums.empty?
    print[board] if safe[board]
  else
    nums.each do |x|
      queen[nums - [x], [x] + board]
    end
  end
}

iota = ->(n) {
  (n == 1) ? [1] : iota[n - 1] + [n]
}

queen[iota[8], []]

高速化版です。

queen_fast = ->(nums, board) {
  if nums.empty?
    print[board]
  else
    nums.each do |x|
      queen_fast[nums - [x], [x] + board] if attack[x, board]
    end
  end
}

queen_fast[iota[8], []]

Common Lisp 版のほぼ忠実な移植で、しかもずっと読みやすくなっていると思います。このような形の関数型プログラミングなら、Ruby で充分可能だということがわかります。
 
かかった時間です。最初のバージョン。

real	0m0.313s
user	0m0.276s
sys	0m0.004s

高速化されたバージョン。

real	0m0.091s
user	0m0.048s
sys	0m0.012s

6倍くらい高速化されていますね。

パスカルの三角形(Ruby)

パスカルの三角形とは、こんな感じのものです。

              1               
             1 1              
            1 2 1             
           1 3 3 1            
          1 4 6 4 1           
        1 5 10 10 5 1         
       1 6 15 20 15 6 1       
     1 7 21 35 35 21 7 1      

規則性がわかりますね。多項式 (a + b)n を展開すると、係数がこんな風に並びます。

Ruby関数型プログラミングっぽく書いてみました。結構きれいに書けます。

pascal = ->(n) {
  each_cons = ->(ar) {
    (ar.size < 2) ? [] : [ar[0] + ar[1]] + each_cons.(ar.drop(1))
  }
  n.zero? ? [1] : [1] + each_cons.(pascal.(n - 1)) + [1]
}

n = 8
n.times {|i| puts pascal.(i).join(" ").center(30)}

 

追記(2020/1/7)

Enumerator を使って。

def generate_pascal_triangle
  Enumerator.new do |y|
    y << [1]
    ary = [1, 1]
    loop do
      y << ary
      ary = [1] + ary.each_cons(2).map { |a, b| a + b } + [1]
    end
  end
end

n = 8
puts generate_pascal_triangle.take(n).map { |ary| ary.join(" ").center(30) }

 
パターンマッチを使って。

def calc_pascal_triangle(*args)
  case args
  in [1] then [[1]]
  in [2, Array => result] then result
  in [Integer => n]
    calc_pascal_triangle(n, [[1], [1, 1]])
  in [Integer => n, Array => result]
    ary = [1] + result[-1].each_cons(2).map { |a, b| a + b } + [1]
    calc_pascal_triangle(n - 1, result + [ary])
  end
end

n = 8
puts calc_pascal_triangle(n).map { |ary| ary.join(" ").center(30) }

パターンマッチ、意外とおもしろいな。無理やりだけれども、型記述や多重ディスパッチみたいなことも可能かも。Ruby に合うかどうかはわからないが。

n進グレイコード(Ruby)

ゆえあって n進グレイコードへの変換式を作ることになった。
 
グレイコード - Wikipedia
グレイコードについてはここにあるとおりである。2進グレイコードなら、ネットを検索すればたくさん出てくる。具体例は、Ruby のハッシュで表すとこんな感じだ。

{"0"=>"000", "1"=>"001", "10"=>"011", "11"=>"010",
 "100"=>"110", "101"=>"111", "110"=>"101", "111"=>"100"}

キーが 2進数、値が 2進グレイコードである。10進数の 3 から 4 は 2進数だと 011 から 100 で、3つのビットが変化している。しかし、グレイコードだと 010 から 110 で 1つのビットしか変化していない。このように、連続する数値ならすべてのビットの変化が 1となるのがグレイコードである。

2進数から 2進グレイコードへの変換は、Wikipedia にあるとおりだ。

「対象の二進表現」と、「それを1ビット右シフトし、先頭に0をつけたもの」との排他的論理和をとる。

C言語なら v ^ (v >> 1) と表現されるとあるとおりである。ちなみにこれは Ruby コードでもまったく同じ表現になる。


n進グレイコードでも同じ筈である。連続するグレイコードはすべての桁の変化を合わせても 1しか変っていない、そういう数になるはずである。しかしこの変換式を作ろうとしても、自分にはむずかしかった。で、ネットに頼ったところ、Ruby で既にすばらしいコードが! なんと実質 1行である。これ以上のコードは書けないので素直にコピペさせてもらいます。

#整数 i から n進 k桁グレイコードを計算する
def to_gray(n, k, i)
  i.to_s(n).rjust(k + 1, "0").chars.each_cons(2)
    .map {|d| ((d[1].to_i(n) - d[0].to_i(n)) % n).to_s(n)}.join
end

#n進 k桁グレイコード表を作る
def gray_code(n, k)
  (0..n ** k - 1).map {|i| [i.to_s(n), to_gray(n, k, i)]}.to_h
end

p gray_code(3, 3)

なお、Ruby なので n は 36 まで可能である。
実行結果。3進グレイコードへの変換である。(3進なので、0 と 2 の差は 1 であることに注意されたい。)

{"0"=>"000", "1"=>"001", "2"=>"002", "10"=>"012", "11"=>"010", 
 "12"=>"011", "20"=>"021", "21"=>"022", "22"=>"020", "100"=>"120",
 "101"=>"121", "102"=>"122", "110"=>"102", "111"=>"100",
 "112"=>"101", "120"=>"111", "121"=>"112", "122"=>"110",
 "200"=>"210", "201"=>"211", "202"=>"212", "210"=>"222",
 "211"=>"220", "212"=>"221", "220"=>"201", "221"=>"202", "222"=>"200"}

コピペ元はここ。

考察もすばらしいので、是非よく読んで頂きたい。


逆変換は一応自分で考えてみた。こんな感じにできた。

def gray_to_num(g, n)
  s = g[0]
  g.chars.map {|x| x.to_i(n)}.inject {|h, x| s += (h = (h + x) % n).to_s(n); h}
  s
end

しかしこれも、関連記事の方にもっといいコードがあった。

def to_n_ary(n, str)
  a = 0
  str.chars.map{|d| ((a += d.to_i(n)) % n).to_s(n)}.join
end

美しい。


グレイコードとは何やら数学的な遊びで、実用的な価値はなさそうに思えるかも知れないが、そうでもないようだ。例えば思考実験として、こんな場合が考えられる。情報の通信過程で、ある種のノイズが入り得るとする。それは、情報のどこかに 1ビットが挿入されるというものだ。例えば 2進数 0100 に 1ビットのノイズが入り、1100 になったとする。これを 10進数に直すと 4 から 12 で、伝達される値が大きく変ってしまうことになる。しかし 2進グレイコードなら 0100 から 1100 は 10進数で 7 から 8 の変化に過ぎず、10進数でも値は 1 しかちがわない。よってノイズの影響が少ない可能性がある(0000 から 1000 へのような場合はさすがにダメだが)。このような場合である。

その日は何曜日? (Ruby)

Ruby で日付から曜日を知るにはどうしたらよいか、遊びで考えてみました。


下のコードを実行すると、こんな感じ。
曜日を知りたい日付をコマンドライン引数にして実行します。

$ ruby what_day_of_the_week.rb 1989.1.8
1989-01-08 は 日曜日です。

引数なしで実行すると、その日の曜日を出力します。

$ ruby what_day_of_the_week.rb
2018-01-26 は 金曜日です。

 
コードはこんな具合です。
what_day_of_the_week.rb

require 'date'

week = %w(日 月 火 水 木 金 土)
d = if ARGV[0]
  a = ARGV[0].split(".").map(&:to_i)
  Date.new(*a)
else
  Date.today
end
puts "#{d}#{week[d.wday]}曜日です。"

標準添付ライブラリ 'date' を使っています。

ソートの交換回数の最小化(Ruby)

アルゴリズム・パズルです。

問題:
例えば 1~3 の数字が任意に並んだ 3桁の数字を、各桁の入れ替えによってソートする場合、その入れ替えの数が最小になるようにソートしたとします。たとえば 231 なら 231→132→123 で 2回の入れ替えになります。これをすべての順列において行い、入れ替え数の合計を出します。例えば 1~3 の場合なら、最小の入れ替え数の合計は 7回になります。
さて、これを 1~7 の 7桁の場合において求めて下さい。

 
Ruby で解いてみました。
q46.rb

h = {"1234567" => 0}
queue =[["1234567", 0]]
while (x = queue.shift)
  st, depth = x
  [*0..6].combination(2) do |i, j|
    st1 = st.dup
    st1[i], st1[j] = st1[j], st1[i]
    next if h[st1]
    h[st1] = depth + 1
    if h.size == 5040
      puts h.values.inject(:+)    #=>22212
      exit
    end
    queue.push([st1, depth + 1])
  end
end

求める最小の入れ替え数の合計は 22212回になります。かかった時間は僕の環境で 0.1秒あまりです。

ソートの目標の "1234567" から出発して、すべての入れ替えを行い、深さを 1ずつ増やしていって最終的にすべてのパターン(7! = 5040 とおり)があらわれるまで繰り返します。そして、最後に合計数を求めます。

ただこれ、幅優先探索で正解が求められるのですが、深さ優先にするとダメなのですよね。どうしてなのかいまひとつよくわからないのが恥ずかしい…。
いや、当り前か。いきなり深く潜ったらもっと早く出ている筈のパターンが深くなってしまうか。

ちなみに、深さの最大値は 6 です。これは考えてみたら当然ですね。7桁なら最大 6回入れ替えればソートできると。