(プログラミング 小手調べ解説編 第2章)
1 から 20までの数を(今回は、横に)並べて印字させる。空白文字を間に入れて区切ること。
従来の手続き型プログラミングの発想の延長だとこうなるだろう
for i in 1..20 do
print i," " # print は、改行をともなわない出力指示(Rubyでは)
end
puts # 最後に改行
多くの言語で、こうした改行なしの出力指示が用意されてはいる。
Rubyのprint メソッドは、複数の引数を渡せるので上記のような使い方になるが、 (言語によっては)
といった書き方を行うことになる。その場合、上記プログラムの2行目はこうなる。
# 1
print i ; print " "
# 2
print i.to_s+" "
なお、print i+" "
のような書き方はRuby(を含む多くの言語)では動かない(JavaScriptだと動くようだが)。
演算子 +
は、両辺が数値データなら数学的加算、両辺が文字列なら文字列の連結、 として同じ記号でも相手によって違う動作をするが、
両辺の型(クラス)が違うときの動作が定まらないのでエラーにすることになる。
最後の20の右に余分なスペースが印字される。スペースだと誰も気がつかないが、 プログラムとしては不完全とも言える。きちんとするなら、たとえば下記のようになる (ちょっとしたことでかなり複雑なプログラムだ)。
for i in 1..20 do
if i<20 then
print i," "
else
puts i
end
end
印字すべきものを(文字列として)作ってから印字する。
この考え方が、本例題の主題。
puts (1..20).to_a.join " "
(ね、シンプルでしょ)
プログラムの解説:
1..20
は Range.new(1,20)
と同じ意味で、 Range オブジェクトを生成するリテラル表記( ..
が演算子として機能している)。
演算子 ..
の優先度(結合の強さ)は低い方なので、 1..20.to_a
と書くと意味が変わってくる (結果的にはエラーになる)ので、Rangeリテラルを括弧で囲む必要がある。
join " "
の部分は、join(" ")
と書いてもいい
(その先にメソッド呼び出しがあるならば括弧で囲むべきだが、メソッドチェーンの最終弾なので省略してある)。
join は 配列(レシーバ、つまりピリオドの右側に置かれたオブジェクト)の要素を すべて、文字列に変換(内部で .to_s を使っている)してからつなぎ合わせる。
joinに引数があれば、その引数を間に挟んで接続する。
ただし、join は、Arrayクラスのメソッドなのでまず、RangeをArrayに( .to_a メソッドを使って)変換してから使う。
考え方の解説:
ついでながら Enumeratorで無限整数列を生成することもできる。
ns=Enumerator.new{|q|n=1;loop{q<<n;n+=1}}
ns.take(20) # など(Haskellと似た発想ね)
ここでは解説を省略する(「Ruby Enumerator」で情報は得られるでしょう)が、
irbで試しに動かしてみて下さい。
無限の流れを作って(整数列はもともと無限の列だからね)、 それを前から順に(手続き的なループではなく)取り出して何か処理をする、 という考え方はこれから重要になってきます。
まず実例。
' '.join(map(str,range(1,20+1)))
Pythonでは、join は、内部で各要素を文字列に変換しないので、事前に変換してから渡す必要がある。
そこで使うのがこの高階関数 map だ。
Python のmap関数は引数を2つとる。
map が返す値は、(以下の例に見るように)特殊なオブジェクト(イテレータと呼ばれる)。
>>> str(1)
'1'
>>> map(str,[1,2,3])
<map object at 0x109a45da0>
>>> list(map(str,[1,2,3]))
['1', '2', '3']
' '.join(list(map(str,list(range(1,20+1))))))
(上記 mapの紹介でも述べたが)Rubyではクラスに属するインスタンスのメソッドとして機能が提供されるが、
python では、同じ機能が関数として提供されていることが多く、同じことをするために関数呼び出しの入れ子で表現する。
ついでながら、Python には each(に相当するもの)がないという話を前章でしたが、 無理やり map で書くことは、可能ではある(結果の値として生成されたリストを捨て去るという無駄なことをすることになるが)。
list(map(print,[1,2,3]))
map を呼ぶだけでは、イテレータ(これから繰り返しをスタートさせる)が作られるだけなので、
それぞれの値を取り出すループを実行に至らせるためには、もうひと手間必要になる(ここでは関数list に渡している)。
console.log([...Array(20).keys()].join(" "))
[...Array(20).keys()].map(n=>n+1).join(" ")
ここでは(概念はすでに紹介済の)高階関数 map を使っている。
map に無名関数を渡す書き方には、省略可能な部分が多々あって、上の
map(n=>n+1)
は、 map((n)=>{n+1})
を簡略に書いたもの (後者が一般的な無名関数の書き方に沿ったもの)。
R,J の各言語のようにいきなりjoinで連結できない。
Haskellでは(よくも悪くも)型検査が厳しいので、まず、
map show [1..20]
で 文字列のリストに変換する。
連結は(joinという名前は別の場所に使われている)Data.List
パッケージにある 関数 intersperse で、空白(文字列)を各要素の間にサンドイッチ状に挟む
import Data.List
-- と、予め準備しておいて(最初に一回行えばいい)
intersperse " " (map show [1..20])
-- または
intersperse " " $ map show [1..20]
これを concat で1つの文字列につなぐ(またはリストを1レベル分平坦化すると考えてもいい)。その上で、出力する。
putStrLn ( concat ( intersperse " " ( map show [1..20] )))
-- または
putStrLn $ concat $ intersperse " " $ map show [1..20]
ここで使った関数 map も高階関数で、第一引数として、1つの要素を変換するための関数(ここではshow)を渡すという使い方をしている。
> :t show
show :: Show a => a -> String
> :t map
map :: (a -> b) -> [a] -> [b]