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##*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"]

(comment) >(1..4).to_a # => [1, 2, 3, 4]

(1..10).step(2).to_a # => [1, 3, 5, 7, 9]

(0..50).step(5).to_a # => [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

(1.1 .. 10).step(1.2).to_a # => [1.1, 2.3, 3.5, 4.7, 5.9, 7.1, 8.3, 9.5] »通りすがりさん
コメントどうもです。あーなんという… そうですRubyにはRange#stepがあったのですよね..

  1. 手抜きですね^ ^;


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS