RubyでHaskellの数列リストを真似てみよう!

HaskellのリストはRubyの配列と同じように
要素をカンマ区切りのカッコで区切って生成できるんだ

Hugs> [1, 2, 3]
[1,2,3]
Hugs> ['a', 'b', 'c']
"abc"
Hugs> ["one", "two", "three"]
["one","two","three"]


だけどHaskellのリストは
Rubyの配列よりもその記法に柔軟性があり
新しい集合を作るための演算式を書けるリスト内包表記や
数列を簡単に生成できる便利な記法があるんだよ


数列を生成する記法は以下のような感じだよ

Hugs> [1..10]
[1,2,3,4,5,6,7,8,9,10]
Hugs> [21..31]
[21,22,23,24,25,26,27,28,29,30,31]
Hugs> ['a'..'m']
"abcdefghijklm"

Haskellでは文字列は文字のリストなので
最後の結果はaからmの文字列になるんだね


Rubyで上の式をそのまま書くと
1つのRangeオブジェクトをもつ配列と解釈されちゃうんだけど
*(splat)展開を使うと同じことができるんだよ

[*1..10] #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[*21..31] #=> [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
[*'a'..'m'] #=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]

これは以前の投稿でも紹介したよね


でもHaskellの数列展開ではさらに
次のようなこともできちゃうんだよ

Hugs> [1, 3..10]
[1,3,5,7,9]
Hugs> [0, 5..50]
[0,5,10,15,20,25,30,35,40,45,50]
Hugs> [1.1, 2.3..10]
[1.1,2.3,3.5,4.7,5.9,7.1,8.3,9.5]
Hugs> ['a', 'c'..'m']
"acegikm"
Hugs> ['A', 'I'..'z']
"AIQYaiqy"

すごいよね?
いわゆる等差数列が簡単にできちゃった


それだけじゃないんだ
等差数列の無限リストだってできちゃうんだよ!

Hugs> take 20 [1, 9..]
[1,9,17,25,33,41,49,57,65,73,81,89,97,105,113,121,129,137,145,153]
Hugs> take 20 ['A', 'D'..]
"ADGJMPSVY\\_behknqtwz"

使うかどうかはわからないけど
なんかかっこいいよねZen-Codingみたいで!



そんなわけで..



Rubyでも
これと似たようなことをできるようにしてみるね


Rubyでは[1, 3..10]も有効な構文なので
これをそのまま展開するのは都合が悪いよね
だからここではArray#to_aを拡張して
数列展開するようにしてみるよ

class Array
  alias __to_a__ to_a
  def to_a
    if [Numeric, Range] === self
      n, range = self
      dist = range.begin - n
      res = Enumerator.new { |y| loop { y << n; n += dist } }
      return res.take_while { |i| i <= range.end }
    end
    __to_a__
  end
end

だいたいこんな感じでどうかな?


配列の要素が数字とRangeのセットの場合に特別な扱いをするよ
if節の条件式の実装はあとで見せるね
最初の数字とRangeの先頭との差distを取って
Enumeratorで等差数列を作るよ
そしてEnumerator#take_whileを使って
Rangeの最後までの数列を返すようにする


if節の条件で使ったArray#===の実装は次のような感じだよ

class Array
  alias __eq__ ===
  def ===(other)
    if self.size == other.size and any? { |item| item.instance_of? Class }
      other = other.to_enum
      return all? { |item| item === other.next }
    end
    __eq__(other)
  end
end


さあ実行してみるよ

[1,2,3,4].to_a # => [1, 2, 3, 4]
[1..10].to_a # => [1..10]
[1, 3..10].to_a # => [1, 3, 5, 7, 9]
[0, 5..50].to_a # => [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

いい感じだね


でもFloatを渡すと..

[1.1, 2.3..10].to_a # => [1.1, 2.3, 3.4999999999999996, 4.699999999999999, 5.899999999999999, 7.099999999999998, 8.299999999999997, 9.499999999999996]

浮動小数点演算における丸め誤差がでちゃうんだ


bigdecimalというライブラリを使うと
丸め誤差の問題を回避できるようなんだけど
ここではFloat#to_iを改良してごまかしてみるね

class Float
  alias __to_i__ to_i
  def to_i(n=0)
    n > 0 ? (self*10**n).__to_i__/10.0**n : __to_i__
  end
end

1.23456.to_i # => 1
1.23456.to_i(1) # => 1.2
1.23456.to_i(2) # => 1.23
1.23456.to_i(3) # => 1.234

Float#to_iが切り捨てする小数点桁数を
引数として取れるようにする


これを使ってArray#to_aを変更しよう

class Array
  alias __to_a__ to_a
  def to_a(decimal=1) # 小数点2位以上は引数を渡す
    if [Numeric, Range] === self
      n, range = self
      dist = range.begin - n
      res = Enumerator.new { |y| loop { y << n; n += dist } }
      return res.take_while { |i| i <= range.end }
                .map { |i| i.to_i(decimal) rescue i }  # ここを追加
    end
    __to_a__
  end
end

[1, 3..10].to_a # => [1, 3, 5, 7, 9]
[0, 5..50].to_a # => [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
[1.1, 2.3..10].to_a # => [1.1, 2.3, 3.4, 4.6, 5.8, 7.0, 8.2, 9.4]
[1.11, 2.32..10].to_a(2) # => [1.11, 2.31, 3.52, 4.73, 5.94, 7.15, 8.36, 9.57]

いい感じだね!


さあ次は
アルファベットの等差数列だ


ここでもArray#to_aはあまりいじりたくないので
Stringクラスで算術演算できるようにしてみよう
つまりString#+ が数字を受け取ったときは
その文字コード分シフトした文字を返すようにする
またString#- を定義して文字を受け取ったときは
その文字コードの差を返すようにする

class String
  alias __plus__ +
  def +(other)
    if other.is_a? Integer
      return (self.ord + other).chr
    end
    __plus__(other)
  end

  def -(other)
    case other
    when String
      self.ord - other.ord
    when Integer
      (self.ord - other).chr
    else
      raise ArgumentError
    end
  end
end

'a' + 5 # => "f"
'f' - 'a' # => 5
'f' - 5 # => "a"

なんとなく汎用性がありそうだよね


こうすればArray#to_aは条件判定の
NumericをObjectに代えるだけでいい*1

class Array
  alias __to_a__ to_a
  def to_a(decimal=1)
    if [Object, Range] === self # NumericをObjectに変更
      n, range = self
      dist = range.begin - n
      res = Enumerator.new { |y| loop { y << n; n += dist } }
      return res.take_while { |i| i <= range.end }
                .map { |i| i.to_i(decimal) rescue i }
    end
    __to_a__
  end
end

['a', 'c'..'m'].to_a # => ["a", "c", "e", "g", "i", "k", "m"]
['A', 'I'..'z'].to_a # => ["A", "I", "Q", "Y", "a", "i", "q", "y"]
[1, 3..10].to_a # => [1, 3, 5, 7, 9]
[0, 5..50].to_a # => [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
[1.1, 2.3..10].to_a # => [1.1, 2.3, 3.4, 4.6, 5.8, 7.0, 8.2, 9.4]
[1.11, 2.32..10].to_a(2) # => [1.11, 2.31, 3.52, 4.73, 5.94, 7.15, 8.36, 9.57]

うまくいったね


さあ
最後は無限リストだ
Rubyでは[1, 3..]という記法は構文エラーになるので
Rangeの最後が-1(文字の場合は'-1')なら
無限リストにするのはどうかな


Array#to_aの変更は簡単だよ

class Array
  alias __to_a__ to_a
  def to_a(decimal=1)
    if [Object, Range] === self
      n, range = self
      dist = range.begin - n
      res = Enumerator.new { |y| loop { y << n; n += dist } }
      unless range.end.to_s.to_i < 0   # ここを追加
        return res.take_while { |i| i <= range.end }
                  .map { |i| i.to_i(decimal) rescue i }
      else
        return res                     # ここを追加
      end
    end
    __to_a__
  end
end

Array#to_aの内部ではEnumeratorを使っているので
Enumerable#take_whileしなければ
そのまま無限リストが返るよ


さあ実行してみよう

[1, 9..-1].to_a.take 20 # => [1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97, 105, 113, 121, 129, 137, 145, 153]
['A', 'D'..'-1'].to_a.take 20 # => ["A", "D", "G", "J", "M", "P", "S", "V", "Y", "\\", "_", "b", "e", "h", "k", "n", "q", "t", "w", "z"]

うまくいったよ!


Haskellには敵わないけど
Rubyも柔軟だってことが
この投稿で伝わったらうれしいよ


(追記:2011-7-9)
ああ Rubyには偉大なるRange#stepがあったんだね
通りすがりさんありがとう!


だからわざわざ上のようなことをしなくても
大体のことはできるんだよ

(1..10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
(1..10).step(2).to_a #=> [1, 3, 5, 7, 9]
(1.1..10).step(1.2).to_a #=> [1.1, 2.3, 3.5, 4.699999999999999, 5.9, 7.1, 8.299999999999999, 9.5]
('a'..'m').step(2).to_a #=> ["a", "c", "e", "g", "i", "k", "m"]
('A'..'z').step('I'.ord-'A'.ord).to_a #=> ["A", "I", "Q", "Y", "a", "i", "q", "y"]


またNumeric#stepもあるから
数字なら以下のように書いてもいいよね

1.step(10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1.step(10, 2).to_a #=> [1, 3, 5, 7, 9]
1.1.step(10, 1.2).to_a #=> [1.1, 2.3, 3.5, 4.699999999999999, 5.9, 7.1, 8.299999999999999, 9.5]


こうなるとString#stepもほしいよね
こんな感じかな?

class String
  def step(last, nxt=self.next)
    x, dist = self.ord, nxt.ord-self.ord
    Enumerator.new { |y|
      until x > last.ord
        y << x.chr
        x += dist
      end
    }
  end
end

'a'.step('m', 'c').to_a # => ["a", "c", "e", "g", "i", "k", "m"]
'A'.step('z', 'I').to_a # => ["A", "I", "Q", "Y", "a", "i", "q", "y"]
'a'.step('m').to_a # => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]

*1:手抜きですね^^;