Ruby で関数型プログラミングっぽく(コピペ) + Haskell 版

parrot.hatenadiary.jpここのブログ記事を読んで感銘を受けました。だからこれを読んでもらえればよいのですが、せっかくなのでコピペしておきます。元記事に感謝です。

まずは問題。

ある数字にそれを逆に並べた数字を足すという計算を、
回文数(上から読んでも下から読んでも同じ数)になるまで繰り返すとき、
もっとも計算回数を要する二桁の数を答えなさい
 
例:ab+ba=123の場合、123+321=444で回文数なので、2回となる

https://parrot.hatenadiary.jp/entry/20110302/1299051431

 
Ruby コードです。多少自己流に書き直しました。

reverse_num = ->(num) {num.to_s.reverse.to_i}
is_palindrome = ->(num) {num == reverse_num.(num)}
execute = ->(count, num) {
  r = num + reverse_num.(num)
  is_palindrome.(r) ? count : execute.(count + 1, r)
}

result = (10..99).map{|n| [n, execute.(1, n)]}
max_count = result.map(&:last).max
result.select{|r| r.last == max_count}.each{|r| puts "num:#{r.first} count:#{r.last}"}

結果。89 と 98 ですね。24回繰り返しています。

$ time ruby palindrome_num.rb
num:89 count:24
num:98 count:24

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

 
いや、すばらしい。
 

Haskell

頑張って Scala ならぬ Haskell で同様のことをやってみました。
palindrome_num.hs

reverseNum :: Int -> Int
reverseNum num = read $ reverse $ show num

isPalindrome :: Int -> Bool
isPalindrome num = (num == reverseNum num)

execute :: Int -> Int -> Int
execute count num = if isPalindrome r
                    then count
                    else execute (count + 1) r
                        where r = num + reverseNum num
                        
main :: IO ()
main = putStr $ unlines $ map toS $ [r | r <- result, last r == maxCount]
           where result = [[n, execute 1 n] | n <- [10..99]]
                 maxCount = maximum $ map last result
                 toS [a, b] = "num:" ++ show a ++ " count:" ++ show b

こんなのでいいのかな。
結果。

$ time ./palindrome_num
num:89 count:24
num:98 count:24

real	0m0.003s
user	0m0.000s
sys	0m0.000s

当然のことながら瞬殺ですな。

しかし、Ruby でも結構関数型っぽく書けますね。ほとんどそのまま Haskell になるじゃん。自分は Ruby で慣れているので、Ruby の方が見やすいくらいだ。以下の記事もよろしければどうぞ。
obelisk.hatenablog.com

 

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者:Miran Lipovača
  • 発売日: 2012/05/23
  • メディア: 単行本(ソフトカバー)
 

追記(2020/12/4)

Ruby 版後半があまり美しくないので、少し凝ってみる。

module Enumerable
  def max_select
    pool = []
    max_num = -Float::INFINITY
    each do |i|
      n = yield(i)
      if n > max_num
        max_num = n
        pool = [i]
      elsif n == max_num
        pool << i
      end
    end
    [max_num, pool]
  end
end

Enumerable#max_selectは、ブロックの返り値の最大値(max_num)を求めて、そのような最大値になるようなものをレシーバーからコレクトし(pool)、[max_num, pool]を返すメソッド。例えばこんな風に使える。ローマ字化した県名のうち、もっとも文字数が長いもの。

url = "https://gist.githubusercontent.com/koseki/38926/raw/671d5279db1e5cb2c137465e22424c6ba27f4524/todouhuken.txt"
prefectures = URI.open(url).each_line.map {|l| l.chomp.split.last}
prefectures.max_select(&:size)
#=>[9, ["fukushima", "yamanashi", "hiroshima", "yamaguchi", "tokushima", "kagoshima"]]

9文字が最大値だとわかる。

まあ、他に使いみちがあるかどうかわからないものだが(笑)、これを使って以下のように。

reverse_num = ->(num) {num.to_s.reverse.to_i}
is_palindrome = ->(num) {num == reverse_num.(num)}
execute = ->(num, count = 1) {
  r = num + reverse_num.(num)
  is_palindrome.(r) ? count : execute.(r, count + 1)
}

count, numbers = (10..99).max_select(&execute)
puts "max count: #{count}"
puts "number: #{numbers}"

ちょっと関数型プログラミングっぽいかも知れない笑。