yunomuのブログ

趣味のこと

「20分ではじめるRuby」解説 4ページ目

前回: 「20分ではじめるRuby」解説 3ページ目 - yunomuのブログ

最後です。

実行

 まずは3ページ目に戻って、「ファイルに保存して実行」です。

このファイルを“ri20min.rb”という名前で保存して、“ruby ri20min.rb”と 実行しましょう。

 メモ帳でもなんでもいいので、MegaGreeterの書いてあるソースコード部分を全部打ち込みます。別にコピペでもいいです。ただ、打ち込んでみて雰囲気を感じるのも最初は良いものです。ここで「メモ帳は使いにくにな」と思って壮大なテキストエディタ探しの旅に出るのも楽しいかもしれません。WindowsだとEmEditorとかTeraPadなどが有名ですが、そういうRubyに対応したエディタだとソースコードに自動的に色をつけてくれたりして、見やすくなることがあります。

 書いたソースコードを保存します。保存場所は、コマンドラインirbなどを実行しているディレクトリ(フォルダ)がいいでしょう。dirコマンドなどを実行するとファイル一覧が表示されると思いますが、その一覧の中に"ri20min.rb"があればとりあえず成功です。実行しましょう。

C:¥Users¥yunomu> ruby ri20min.rb

こんな感じで。

 例の通りに表示されていれば成功です。なんかエラーが出た場合は、中身を見てみましょう。FileNotFoundErrorならファイル名(ri20min.rb)を間違えているか、保存場所が違います。SyntaxErrorやNameErrorやNoMethodErrorの場合は、どこかに打ち間違いとかがあります。メッセージで出ている場所の近辺を探してみましょう。

コメント

Rubyでは、ハッシュマークで始まる行の内容は コメントになり、インタプリタからは無視されます。

'#'で始まる行は意味が無いので何を書いてもいいよという話です。マニュアルでは、Ruby言語仕様の「字句構造」のところにあります。内容は1行くらいしかありません。

次の一文、

このファイルの最初の行は 特別な行で、Unix系のOSではファイルをどう実行するかをシェルに知らせてくれます。

これは、先ほど書いたソースコード(ri20min.rb)の一行目のこれのことです。

#!/usr/bin/env ruby

 これはUnix系のOSの時だけ関係ある話で、別に無くても構いませんし、Windowsとかだと無意味だったり書き方が違ったりします。まあ書かなくてもいいでしょう。

if, elsif, else, end

 say_hiの中でifを使っていますね。if文です。これは条件分岐といって、「晴れていたら外に出かける。そうでなければ家でゲーム」みたいなやつです。条件分岐が出てくるとかなりプログラミングっぽさが出てきます。

 マニュアルはRuby言語仕様の「制御構造」のページです。条件分岐の節の最初にifの項目があります。とりあえず例をirbかなにかで書いて実行してみるとよいでしょう。制御構造は慣れていても書き方をよく忘れるのでよくお世話になるページのひとつです。

 今まではなんとなく流していましたが、「文法」と書いてあるブロックを見てみましょう。

if 式 [then]
  式 ...
[elsif 式 [then]
  式 ... ]
...
[else
  式 ... ]
end

 これはif文の書き方を示しています。[]で囲まれている部分は、あってもなくても良いという意味です。なのでthenは書いても書かなくても良いということです。面倒なので書かなくてよいです。

 say_hiでは一番複雑な、if, elsif, elseが全部登場するパターンで書かれています。つまり一番シンプルなのは以下のパターンです。式がtrueなら"hello"と出力し、falseなら何もしません。

if 式
  puts "hello"
end

 「式」の部分には、例にあるように、truefalseになる何らかの式が入ります。age >= 12ageが12以上ならtrueということです。true(真)false(偽)になる値のことを真偽値(bool)と言います。

 もちろん>=にもマニュアルがあります。これはIntegerのメソッドなので、Builtin librariesのIntegerクラスのページにあります。

self >= other -> bool
比較演算子。数値として等しいまたは大きいか判定します。

 これは、age >= 12の例で説明すると、self(age: Integerの数値)がother(12: これもIntegerの数値)以上どうかを真偽値(bool)で表すという意味で、正しければtrueという意味になります。要するに数学の"≧"です。

 elsifは、おそらく"else if"の略で、最初のifの条件で引っかからなかった場合にもう一度条件を書く場合に登場します。例えばこのように使います。

if age < 12
  # ageが12より小さい時の処理
elsif age < 15
  # ageが12より小さくなくて、15より小さい時
elsif age < 18
  # ageが12より小さくなくて、15より小さくなくて、18より小さい時
else
  # それ以外
end

 このように、elsifはいくつでも書くことができます。elseはそれ以外の場合がある時に1つだけ書くことができます。elsifelseも省略しても構いません。

演算子

 ついでに、演算子式について。

 メソッドの中には演算子と呼ばれるものがあります。演算子メソッドは特別な書き方ができます。例えば、+はIntegerのメソッドですが、普通のメソッドのように1.+(2)と書くこともできますが、1 + 2と書くこともできます。これが実は特別な話だったのです。

 演算子といえば、一般的には+, -, *, /などがありますが、Rubyには他にも色々あります。

 そのあたりのマニュアルは、Ruby言語仕様の「演算子式」のページにあります。

@names.nil?

 say_hiのif文の式のところに書いてある@names.nil?とはなにか。@namesは一旦initializeの中で定義されている変数だと思ってください。その上で@names.nil?は、Builtin librariesのObjectクラスのページに説明があります。

nil? -> bool
レシーバが nil であれば真を返します。

 つまり、@names == nilと同じ意味です。ここでnilというのは、無です。他の言語ではnullだったりします。つまり@names.nil?@namesが無(nil)だった場合にtrueになります。

 さらにnilとは何か。nilはNilClassのオブジェクトです。Builtin librariesのページからNilClassのマニュアルを探して見てみましょう。メソッドのマニュアルを見てみると、おおむね虚無っぽい挙動をしていることがわかると思います。

 nilはどのような時に使うのか。それは、変数を作りたいけどまだ具体的な値を入れたくない時に使います。例えば以下のコードのように、tallの値によって内容を変えたい時などです。

a = nil # 変数aを作りたいが中身がまだ決まっていない。
if tall > 150
  a = "anchan"
else
  a = "ainachan"
end
puts a

 ただしこの場合は、if文が値を返すので以下のように書いた方が良いです。(if文のマニュアルを確認してください)

a = if tall > 150
  "anchan"
else
  "ainachan"
end
puts a

 でも似たような場合でもa = nilと書いてから何かやった方が便利な時もあるので、そういう時のためにnilというものが存在します。プログラミングはひとつのことをやるためにいくつもの答えが存在します。その中から適切なものを選ぶために、良い例も良くない例も一応見ておきましょう。上の例でどちらがいいのかは、ちょっこれだけではなんとも言えないというのが正直なところです。

最後に'?'がついているメソッド

 マニュアルをざっと見てまわった方はお気づきかもしれませんが、メソッドの中には'?'で終わるメソッドがあります。@names.nil?もそうですし、例えばIntegerには結構たくさんあります。

 Integerクラスのマニュアルを見てみましょう。インスタンスメソッドの項目にeven?odd?があります。これは偶数か奇数かを判定するメソッドです。他にも、Numberから継承したメソッドの項目にはpositive?negative?なんかもあります。意味はマニュアルを見ればわかるでしょう。継承についてはまた別に説明します。

 '?'がついているメソッドには共通の特徴があります。「bool値を返す」ということです。つまり'?'がついたメソッドはtruefalseの値を返します。

 これは厳密にはルールではないのですが、お約束です。「'?'がついているメソッドではbool値を返すようにしよう」という暗黙の約束がRubyの世界にはあります。別に守る必要は無いのですが、守った方がみんなわかりやすくて便利だよねということです。Rubyに限らずプログラミング言語にはこういうお約束が時々あります。会社やチームによって決められることもあります。特に守る必要も無いのですが、一人の時は自分ルールを持っておくと後で助かることもあるかもしれません。チームにルールがある場合はそちらに従うと良いでしょう。そういうあいまいなことも時々あります。

おわり

 「20分ではじめるRuby」の4ページ目が、最初のsay_hiメソッドの途中で終わりました。次回はつづきで、4ページ目その2という感じで続けていきます。その3くらいまでいきそうな気がしますがよろしくお願いします。

「20分ではじめるRuby」解説 3ページ目

前回: 「20分ではじめるRuby」解説 2ページ目 - yunomuのブログ

オブジェクトを作る

それではgreeterオブジェクトを作り、使ってみましょう。

から始まります。最初から飛ばしていますね。

 2ページ目の最後にclass Greeterを書きましたね。これがクラスだという話でした。クラスとは何か。クラスとは、「整数」とか「文字列」のような、値の形のようなものです。「型」と言う時もあります。

 1ページ目の時に「整数(Integer)」のマニュアルを見ましたが、そのIntegerもクラスです。マニュアルのBuiltin librariesのページで「クラス」の節の中からIntegerを探したのを覚えているでしょうか。Integerはクラスなので、マニュアルのクラスの節に書かれています。逆に、クラスの節に書かれているものは全てクラスなので、ここからの説明はおおむねあてはまります。そしてクラスはここに並んでいる以外にも自分で作ることもできます。つまりその作ったものがGreeterです。

Greeter.newのところから見てみましょう。

irb(main):035:0> greeter = Greeter.new("Pat")

 このnewというのは、クラスを元にオブジェクトを作るというメソッドです。オブジェクトというのは、クラス(型)に対する値のことで、つまりIntegerクラスのオブジェクトが23で、Stringクラスのオブジェクトが"あいなちゃん"とか"anchan"とかになるイメージです。わかりますか。

 IntegerやStringはリテラル(前回出てきた)でオブジェクトを作れますが、それ以外のほとんどのクラスではnewメソッドを使ってオブジェクトを作ります。

 newメソッドのマニュアルはどこにあるかというと、おおむねほとんどのクラスにそれぞれあります。Stringにもあります。適当なクラスのマニュアルを探して見てみましょう。「特異メソッド」の項目にあるはずです。ただし、Integerにはnewはありません。例外的にそういうクラスもあります。例外というのはどこにでもあります。そういうものだと思っておきましょう。

 その上でGreeter.new("Pat")が何をしているか。newは実は2ページ目で書いたinitializeを呼び出しています。ここの部分↓

irb(main):025:1>   def initialize(name = "World")
irb(main):026:2>     @name = name
irb(main):027:2>   end

 その後に出てくるgreeter.say_higteeter.say_byeはそのままsay_hisay_byeを呼び出していますが、newだけはinitializeを呼び出します。これはnewが特別だからで、その件についてはObjectクラスのマニュアルの中に書かれています。ややこしいですが、オブジェクトという用語とは別にObjectという名前のクラスが存在します。Builtin librariesのページのクラスの節からObjectのマニュアルを見てみましょう。

 「privateメソッド」と書かれている節にinitializeがあると思います。ここに、要するにここで説明したようなことが書いてあります。newを実行するとinitializeが実行されます。何故そんなことになっているのか気になる場合は聞いてくれれば記事が増えます。

SyntaxError

irb(main):038:0> greeter.@name
SyntaxError: (irb):38: syntax error, unexpected tIVAR, expecting '('

 これはsyntax、つまり文法のエラーです。Rubyとしての書き方がなにか間違えている時に出るエラーです。エラーメッセージでは「'('がくるはずだったのになにかおかしい」というようなことが書かれていますが、そもそもRubyの規則に則っていないのでSyntaxErrorの場合のメッセージはあまり当てにならないことが多いです。'('や')'の書き忘れというパターンが多い気がします。がんばって探しましょう。

Objectの殻の中

 ここではGreeter.instance_methodsを使って呼び出すことができるメソッドの一覧を出しています。この後、Greeter.instance_methods(false)falseを渡して見やすくしていますが、その前の大量に出てきた出力は何か。それこそが、マニュアルのクラスObjectのページで書かれていたメソッドたちです。

引数falseを渡します。 これは祖先のクラスで定義されたメソッドが不要であることを意味します。

 と説明にあります。この「先祖のクラスで定義されたメソッド」という言葉の中の「先祖のクラス」というのがつまりObjectクラスです。Objectクラスは自分で定義したものも含めて全てのクラスの先祖になります。自動的になります。Rubyではそういうルールになっています。そのことは、Objectクラスのマニュアルの「要約」の節に書かれています。これはそういう意味です。

クラスの変更 - まだ間に合います

 ここではattr_accessorが出てきます。このマニュアルはBuiltin librariesのクラス節のClassのページにあります。クラスそのものもオブジェクトで、言わばClassクラスのオブジェクトなのです。これが重要になるのは何か既存のものの動きを書き換えたいとかそういうマニアックな欲求がある時なので、そういうのも後でいいでしょう。Rubyはなんでもアリなところがあるのでこういう機能はちょいちょい出てきます。

MegaGreeterあたりのこと

 ここで急に「ファイルに書いてみましょう」と言われて驚いたかと思います。その前にひとつ。

IRBを抜けるには、“quit”や“exit”とタイプするか、コントロールキーを押しながらDキーを押します。

 ここで出てくる"quit"はIRBの特殊なコマンドですが、exitRubyの機能です。マニュアルで言うとBuiltin librariesのモジュールKernelにあります。前回(2ページ目)の記事で解説したKernelモジュールです。ここにexitも「Rubyプログラムの実行を終了します。」と書いてあります。

 それともうひとつ、「コントロールキーを押しながらDキーを押します。」について。これは専門用語で、Ctrl-DとかC-Dとか^Dとも書きます。「ここでデータは終わりですよ」という意味です。IRB以外のコマンドでも使えることがあるのでたまに思い出すといいかもしれません。

 これとは別に「コントロールキーを押しながらCキーを押す」というのもあります。Ctrl-CとかC-Cとか^Cとも書きます。これは制御不能になった時に押します。具体的には無限ループになってしまった時などです。

 以下のコードをIRBで実行してみましょう。

irb(main):011:1* while true
irb(main):012:1*   puts "ainachan"
irb(main):013:0> end

 これが"ainachan"連呼野郎です。止まりません。制御不能です。なのでC-Cで止めます。止まりましたよね? 止まらなかったらごめん。どうしようもないわ。

おわり

 MegaGreeterについては次回でよろしく。

次: 「20分ではじめるRuby」解説 4ページ目 - yunomuのブログ

「20分ではじめるRuby」解説 2ページ目

前回: 実際のところ20分ではじまるかもしれないけどよくわからないよねRuby - yunomuのブログ

つづき。「20分ではじめるRuby」の今回は2ページ目についてです。

今回もマニュアルを見ながらいきましょう。

docs.ruby-lang.org
www.ruby-lang.org

メソッド定義

 メソッドを定義しましょう。こういう感じのやつです。

def hi(name)
  puts "Hello #{name}!"
end

 まず「メソッド」というのは、これまで機能とか関数とか言っていたやつのことです。専門的には少し違いがありますが、Rubyのマニュアル上ではほとんど同じような意味で書かれているので、だいたいメソッドとか関数とか言っておけば通じると思います。前回出てきたputsMath.sqrtや、四則演算の+*なんかもメソッドです。

 定義というのは、「書く」くらいの意味です。別に「作る」でもよいです。

 メソッド定義のやり方は、前回の組み込みライブラリの話とは違い、リファレンスマニュアルのトップページの「Ruby言語仕様」の節にあります。Rubyの文法の「クラス/メソッドの定義」のところです。その先のページの「メソッド定義」の節で書き方が説明されています。例がいくつかありますが、今回の例に近いのはhellofooが出てくるやつだと思います。メソッド定義のやりかたを忘れたらこのページに戻ってきましょう。気になる例を自分で書いてみるのもいいかもしれませんが、結構高度なものもあるのでまだよくわからないかもしれません。機会があれば別に説明します。

「Stringに穴を開ける」?

 表現が謎ですが、先程のhiメソッドでnameを文字列に埋め込んで"Hello #{name}!"としていたやつについて。やってみておわかりのとおり、埋め込んでいます。この埋め込みは、文字列の機能だからStringの機能かと思いきや、実は違って、これも「Ruby言語仕様」の中にあります。「リテラル」の項目です。

 このように、機能が見つからない時はもしかしたら言語仕様の中にあるかもしれないというのはRubyではよくあるので、「そういうものか」くらいに思っておいてください。

 リテラルとは、プログラムの中に書かれた値そのものです。具体的には"Hello World"という文字列とか、2とか3とかの数値とかのことです。例えば文字列の場合、"'で囲むとRubyプログラムの中で使える文字列になります。数字はそのまま書いただけで数値になります。こういうルールや仕組みを「リテラル」と呼びます。

 このマニュアルのリテラルのページには、どういう書き方をすると文字列や数値として認識されるのか、とか、例に出てきたような埋め込みの書き方などが書かれています。具体的には、埋め込みは「文字列リテラル」の節の「式展開」の項目に書かれています。

 文字列リテラルについては、ちょっと面白いのは式展開の前の項目の「バックスラッシュ記法」です。例えば以下のような文字列を表示したい場合

よっしゃよっしゃ
わっしょい

 つまり、改行をしたい場合にどうするか。putsは必ず最後に改行するのでputsを2回書いてもよいです。

puts "よっしゃよっしゃ"
puts "わっしょい"

 でもこれをバックスラッシュ記法を使うと1行でも書けます。

puts "よっしゃよっしゃ\nわっしょい"

 このように、\nを書いた部分が改行になります。文字列を"よっしゃよっしゃ\nわっしょい\a"にすると、表示は変わりませんがなんか「ピ」とか音が鳴るかもしれません。環境によっては鳴らないかも。音はともかく、改行は便利なので覚えておくといいと思います。

変数について

 そういえば、前回の「20分ではじめるRuby」1ページ目の最後に変数が出てきていました。こういうやつです。

irb(main) > a = 3 ** 2

 計算結果をaに格納して使い回すことができるやつです。変数は数値や文字列と同じように関数に渡したり(puts a, hi(a))、文字列に埋め込んだり("Hello #{a}")できます。

 変数については、Ruby言語仕様の「変数と定数」の項目にあります。前回出てきたのはこのページの中の「ローカル変数」というやつです。見てみると「名前は小文字で初めましょう」とか書いてあると思います。ここには主にそういうルールと、あとスコープについて書かれています。スコープというのは、その変数を使うことができる範囲のことです。定義する前に使ってはいけないとか、有効になってない場所でも使えないとか、そういう話です。詳しくはサンプルコードを動かしてみてください。

 この「変数と定数」のページでは、他にも色々な種類の変数を扱っています。書き方が変わるとルールやスコープが変わってきます。そのうち出てくるので新しいものを見かけたらまたこのページに戻ってきてもいいでしょう。「20分ではじめる」でもひととおり出ているので、このページのサンプルコードを書いてみるというのもいいかもしれません。

Greeterあたりのこと

 ここでclassが出てきます。最初からやってきて、このあたりでよくわからなくなった人も多いんじゃないでしょうか。実際、クラスを書くという時はプログラムが結構大きくなった時とか大きいものを作ろうとしている時が多いので、覚えてもしばらくはあまり登場機会は無いかもしれません。ただ、それなりのものを作ろうという時や、既存のプログラムを改造する時などには必ず出てくるので、一応やっておいてもいいでしょう。

 書き方などは、言語仕様の「クラス/メソッドの定義」のページです。メソッドの時に出てきたページです。結構色々できるんですが、それはまあ追い追い。

 ここで@nameというのが出てきますね。これはインスタンス変数といって、これも言語仕様の「変数と定数」のページで出てきます。クラスの中でだけ使える変数です。説明は次のページでちょっとありますね。

おわり

 これで2ページ目もおわりです。意外と書くことが多いですね。

つづき: 「20分ではじめるRuby」解説 3ページ目 - yunomuのブログ

実際のところ20分ではじまるかもしれないけどよくわからないよねRuby

まえがき

 プログラミングをはじめてみようと思ってとりあえず日本語の解説が多くて名前もいい感じだからRubyを選んでチュートリアルをひととおりやってみようと思って「20分ではじめるRuby」をひととおりやってみたみなさん。実際のところはインストールして最初にirbで"Hello World"とか言った段階で20分なんてとうに過ぎてしまったのではないでしょうか。このシリーズでは、「20分ではじめるRuby」をとりあえず最後まで読んでみたという人を対象にしています。まずはわからなくてもいいのでひととおりあそんでみてください。

www.ruby-lang.org

 ここでは、「20分ではじめるRuby」をもとに、それぞれの項目で何をやっていたのかを、Rubyのドキュメントを見ながら解説しようと思います。Rubyのバージョンは2.7.0で、マニュアルはこれを使います。

docs.ruby-lang.org

 マニュアルは、Ruby公式サイト(https://www.ruby-lang.org/ja/)の「ドキュメント」のリンクから「RubyリファレンスマニュアルRuby 2.7.0版」のようなリンクを辿って見ることができます。今後やっていく上で一番大事な情報なのでよく覚えておいてください。プログラミングの作業のほとんどはこのマニュアルを辿ることと言っても過言ではないでしょう。マニュアルを自由に読むことができ、やりたいことを見つけられるようになったとき、その人は初心者を越えてエースになっていることでしょう。

 ではまず「20分ではじめるRuby」の1ページ目からいきます。

putsについて

 "Hello World"は飛ばして次、putsが出てくるところ。

irb(main) > puts "Hello World"

 ここでputsは「Rubyで何かを出力する基本的なコマンドです」と説明されています。これが何なのか、マニュアルを見ていきます。

 リファレンスマニュアルのページを開いてください。Rubyの基本的な機能については、リファレンスマニュアルの中の「組み込みライブラリ(Builtin libraries)」のページに書かれています。組み込みライブラリのページを開くと要約にこんなことが書いてあります。

要約
組み込みライブラリは Ruby 本体に組み込まれているライブラリです。このライブラリに含まれるクラスやモジュールは、 require を書かなくても使うことができます。

 要するに、ここに書いてある機能は基本機能なのでいきなりirbやファイルに書いても使うことができますということです。

 その中でも実はputsみたいなものはもう少し特殊で、このページの中の「モジュール」の節の「Kernel」というところに書かれています。ちなみにKernelというのは核のことで、つまり基本機能の中の基本機能ということです。

 Kernelのページを開いて見てみましょう。「目次」の「モジュール関数」の部分を見ると、この中にputsもありますね。基本的にこの中にリストアップされている関数(機能)は、putsと同じようにirbやファイルの中でいつでも使うことができます。

 例えばこの中に、puts以外にもいくつか文字を画面に表示する機能があります。pとかprintあたりが一例です。以下のように、putsと書いていた部分を置き換えて動かしてみましょう。

irb(main) > puts "Hello World"
irb(main) > print "Hello World"
irb(main) > p "Hello World"

 似たような結果になると思いますが、少しずつ違うところもあったりします。どう違うのか? ということが、つまりこのマニュアルに書かれています。気になったら読んでみてもいいでしょう。わからない単語だらけかもしれませんが、それはまた聞いてくれれば教えます。

 ということで、プログラムを書いていて「この機能は基本機能にあるんじゃないかな?」と思った時はこのKernelのページのことを思いだしてみてください。それぞれの関数(機能)ごとにちょっとしたサンプルコードも書かれているので、このページを上から下までひととおり見てみるというのも、最初の練習にはいいかもしれません。

整数と足し算

 次は、3+2です。足し算やかけ算をしている部分について。これにもマニュアルがあります。整数の足し算です。

 整数の足し算はKernelの基本機能ではなく、「整数」の機能として作られています。整数、つまり英語で言うとIntegerです。Integerというモジュールがあります。組み込みライブラリ(Builtin libraries)のページに戻って、「クラス」の節からIntegerを探してください。

 Integerのページの目次の「インスタンスメソッド」の項目を見てください。足し算(+)、引き算(-)、かけ算(*)、割り算(/)、累乗(**)、絶対値(abs)、比較(<,>,>=,==)など、色々な機能が列挙されています。3+2などと書いた時は、この機能を使って答えを出していました。ここにもそれぞれサンプルコードが書かれているので試してみてもいいかもしれません。整数の機能だけあってほとんどが計算に関するものですが、中にはtimesのように少し雰囲気が違うものもあります。

Mathモジュール

 次。「20分ではじめるRuby」の1ページ目最後に出てくるMathです。

 Mathは、組み込みライブラリ(Builtin libraries)のページのモジュールの節の中にあります。開いてみると、Integerの時と同様に目次に関数が並んでいます。例題に出てきたsqrtもあります。ここには、数学で使うような少し複雑な計算が色々と定義されています。PI(円周率)なんかもここに定義されています(10桁くらいですが)。難しい計算をしたくなったらここを眺めるといいかもしれません。

 そして、お気付きかもしれませんが、例題に出てきたMath.sqrtと似たようなものがIntegerにもあります。Integer.sqrtです。こちらの場合は整数なので、答えも整数になります。小数点以下切り捨てです。見くらべて見ましょう。

irb(main) > Math.sqrt 5
irb(main) > Integer.sqrt 5
irb(main) > Math.sqrt 4
irb(main) > Integer.sqrt 4

クラスとモジュール

 マニュアルを眺めていたら「クラス」と「モジュール」という言葉が出てきていました。組み込みライブラリのページでも節が分かれていました。違いは何なのか。

 大まかに言うと、整数(Integer)や文字列(String)のように、データに関連しているものがクラスの項目に書かれていて、そうじゃないやつ(MathやKernel)がモジュールの項目に書かれています。慣れないうちはまあページ検索で適当に見つけてやればいいでしょう。

おわり

 とりあえず「20分ではじめるRuby」の1ページ目はこれでおわりです。

次回: 「20分ではじめるRuby」解説 2ページ目 - yunomuのブログ

研究室の思い出 印刷された大量のソースコード

 2004年頃、大学4年で研究室に配属され、OSとネットワークの研究室でちょっと専門外の機械学習の研究をしていた。配属直後なので研究というより概念のお勉強から始まる。

 その研究室では既に10年続くプロジェクトもあり、打ち合わせのために作られた大量の検討資料がファイルに綴じられて本棚に刺さっていた。しかしよく見ると半数以上のラベルがプロジェクト名ではなく、「FreeBSD src/sys/alpha」とか「FreeBSD src/sys/boot」とか、何かのパス名みたいなものだった。そういうファイルが本棚の1列分以上ある。なんだこれ、と思って先輩に聞いてみた。

「あのファイル何なんですか?」
「あーそれ、俺が印刷した」

 この研究室は私が配属される2年前に教授が他の大学から移られてきてできたものであり、先輩は何もわからないままその研究室に入った人だった。教授が前にいた大学に研究室の先輩と言える人はいたものの、そう頻繁に連絡が取れるわけでもなく、先輩のいない研究室でいきなりOSの研究を引き継ぐことになった。当初はマシンの起動やカーネルのビルドで手こずって遠方の先輩に電話で助けてもらったり出張してきてもらったりもしていたらしい。

 そんなこんなで先輩はOSの研究に加わった。前任者の関係だったか何か忘れたけど、提案手法の実装評価はFreeBSDを改造してやることになっていたため、そもそもの構造の勉強をFreeBSDをベースに行おうとした。行おうとしたが、そもそも何をしたらいいのか取っ掛かりがない。どうしたらいいか先生にたずねてみても「ソースコード読め」くらいしか言われない。とりあえず読みやすいように印刷して読んでみるも、当然のように何もわからない。当時もetagsのようにソースコードを解析して定義を参照できるようなツールはあったものの、当時の先輩はそれを知らないし、カーネルソースコード全体を解析できるようなものではない。関数マクロは解析しきれないし、その後いろいろ理解してから解析対象を絞っても2日くらいはかかった。

 先輩は先生に相談してみる。
「どうしたらいいですか」
「関係ありそうなところを読めばいいよ」
「それがどこかわからないんですが」
「じゃあ全部読め」
「印刷していいですか、読みやすいので」
「いいよ」
「本当に? 大量にありますよ? かなり紙を使いますよ?」
「紙はいっぱいあるから」
というようなやり取りを経て、印刷作業が始まったらしい。

 ちょっとした脅しというか、面倒だから教えてほしいと思っていたのに「やってみれば」みたいな対応をされたのでそれに対抗して、でもまあやるしかないかということでソースコードの先頭(アルファベット順)のファイルから印刷開始。ところがソースコードの量は思っていた量の何倍どころか何十倍も多く、数日経ってもsys以下のディレクトリ1つか2つしか消化できていない。でもやれと言うんだからやってやると意地になって1〜2週間、ひたすらプリンタを専有して印刷し続けて、さすがに頭が冷えたらしい。紙も時間も無駄だし、そもそもそれだけやり続けてもまだ1割も行ってない気がするし、そもそもこれを全部読むのかと。

 結局その後、きちんと調べるべき部分を調べ直し、ようやく研究を開始することができ、この時に印刷したソースコードは記念にファイリングして取っておいている、ということらしい。先輩は「いやーあの頃は若かったなー」と言っていたが1年前の話であるし、実際には「きちんと調べるべき部分を調べ直し」の部分の方が大変だったであろうことは想像できる(私もこの1年後に似たようなことをやったので)。

 余談だが、FreeBSDソースコードはsrc/sys以下にあるが、この中にはデバイスドライバや各アーキテクチャ依存のコードもフラットに置かれているため、先輩が本来読むべきコードは "i386", "kern", "ufs"あたりであり、特に研究テーマがファイルシステムに関係が深かったために最も重要なのは"ufs"であった。アルファベット順に読んでいると大変なことになっていただろうし、実際に頭文字cのディレクトリの数個目で止まっていた。cで始まるディレクトリは多い割にほとんど本筋ではない。危ないところだった。

 ということで、この話は面白すぎるので新しく研究室配属されてきた人に「あのファイル何ですか」と聞かれる度に語り部をやっていたが、語る相手もいなくなったので放流することにする。

Goでsubcommandsを使う

gitkubectl みたいなサブコマンドを実装したい時。

 たぶん一番楽なのはsubcommandsライブラリを使う方法だと思います。

subcommands package · pkg.go.dev

 mainは後回しにして、subcommand側から。 Command 型を作って、subcommands.Command インタフェースを実装します。

package subcmd

import (
        "context"
        "flag"
        "fmt"

        "github.com/golang/subcommands"
)

type Command struct {}

// これがサブコマンド名になる
func (c *Command) Name() string { return "sbcmd" }

// コマンド一覧で出てくるサブコマンドの説明
func (c *Command) Synopsis() string { return "subcommands example" }

// helpとかで出てくる使い方
func (c *Command) Usage() string { return "subcmd [args]" }

// flagライブラリでオプションの処理をするやつ
func (c *Command) SetFlags(f *flag.FlagSet) {
}

// 本体
func (c *Command) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
        fmt.Println("ノ˘ω˘) You just don't have enough fight in you!")

        return subcommands.ExitSuccess
}

 mainでこのCommandを登録します。

package main

import (
    "context"
    "flag"
    "os"

    "github.com/google/subcommands"

    "yunomu.net/subcmdex/subcmd" // Commandを定義したパッケージ
)

// 別にmain()でもよい
func init() {
        // commands, flags, helpというサブコマンドはプリセットがあるのでせっかくだから登録する
    subcommands.Register(subcommands.CommandsCommand(), "help")
    subcommands.Register(subcommands.FlagsCommand(), "help")
    subcommands.Register(subcommands.HelpCommand(), "help")

    subcommands.Register(&subcmd.Command{}, "")

        // subcommands.Registerの後に実行する
        // 全体で1回だけ実行しないとおかしなことになる
    flag.Parse()
}

func main() {
    ctx := context.Background()

    subcommands.Execute(ctx)
}

 これでサブコマンドが実行できる。

% go run main.go subcmd
ノ˘ω˘) You just don't have enough fight in you!

 さらにsubcmdのサブコマンドも作れる。CommandインタフェースのSetFlagを以下のように書き換える。

// (省略)

type Command struct {
        cdr *subcommands.Commander // Commanderを保存できるようにする。
}

// (省略)

func (c *Command) SetFlags(f *flag.FlagSet) {
        // SetFlagsはCommandがRegisterで登録された時に呼び出されるのでCommanderはここで初期化する
        cdr = subcommands.NewCommander(f, "")

        cdr.Register(&subsubcmd.Command{})

        // サブコマンドにもcommands, flags, helpがあるのでついでに登録しておくといい
        cdr.Register(cdr.CommandsCommand(), "help")
        cdr.Register(cdr.FlagsCommand(), "help")
        cdr.Register(cdr.HelpCommand(), "help")

        c.cdr = cdr
}

func (c *Command) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
        // Commanderを実行する
        return c.cdr.Execute(ctx, args...)
}
// (省略)

 これで何段階でも階層化したコマンドを作ることができる。

 それはそうと kubectlgcloud コマンドはsubcommandsではなくCobraを使っています。ソースコードを生成するタイプなのでそれなりの命名規則などのクセはあるもののこれはこれで便利。

GitHub - spf13/cobra: A Commander for modern Go CLI interactions

glide getやglide upが動かない

 glide getで新しいライブラリをインストールしようとするとこんなエラーが出た。

[ERROR] Error scanning github.com/golang/protobuf/ptypes/duration: open /Users/yunomu/.glide/cache/src/https-github.com-golang-protobuf-ptypes-duration: no such file or directory
[ERROR] This error means the referenced package was not found.
[ERROR] Missing file or directory errors usually occur when multiple packages
[ERROR] share a common dependency and the first reference encountered by the scanner
[ERROR] sets the version to one that does not contain a subpackage needed required
[ERROR] by another package that uses the shared dependency. Try setting a
[ERROR] version in your glide.yaml that works for all packages that share this
[ERROR] dependency.

 私がメインで使っているMacだとこうなるけど、他の環境ではこのエラーは起きない。

 そもそもエラーメッセージの中では https-github.com-golang-protobuf-ptypes-duration を探そうとしているが私が探しているのは https-github.com-golang-protobuf/ptypes/duration である。パスの切り方が違うので当然ながらファイルは見つからない。

 バグかと思ったけどもそういうissueも登録されていないのでコードを見てみる。 https://github.com/Masterminds/glide/tree/v0.12.3

 件のエラーメッセージが dependency/resolver.go の543行目あたりに書かれている。 https://github.com/Masterminds/glide/blob/v0.12.3/dependency/resolver.go

 543             } else if strings.Contains(errStr, "no such file or directory") {
 544                 r.hadError[dep] = true
 545                 msg.Err("Error scanning %s: %s", dep, err)
 546                 msg.Err("This error means the referenced package was not found.")
 547                 msg.Err("Missing file or directory errors usually occur when multiple packages")
 548                 msg.Err("share a common dependency and the first reference encountered by the scanner")
 549                 msg.Err("sets the version to one that does not contain a subpackage needed required")
 550                 msg.Err("by another package that uses the shared dependency. Try setting a")
 551                 msg.Err("version in your glide.yaml that works for all packages that share this")
 552                 msg.Err("dependency.")

エラーメッセージに “no such file or directory” という文字列が入っているかどうかでエラーを判断しているあたり大胆だ。

 そのエラーの原因がここ。デバッグメッセージ付きで実行してみると、同ファイルのこの部分の時点で 493行目の r.Handler.PkgPath(dep)https-github.com-golang-protobuf-ptypes-duration を返しており、成功するはずがない。

 492         // Here, we want to import the package and see what imports it has.
 493         msg.Debug("Trying to open %s (%s)", dep, r.Handler.PkgPath(dep))
 494         var imps []string
 495         pkg, err := r.BuildContext.ImportDir(r.Handler.PkgPath(dep), 0)

 次に PkgPath の定義を見る。interfaceなので定義は2つあるが repo/installer.go の方。 https://github.com/Masterminds/glide/blob/v0.12.3/repo/installer.go

 600 // PkgPath resolves the location on the filesystem where the package should be.
 601 // This handles making sure to use the cache location.
 602 func (m *MissingPackageHandler) PkgPath(pkg string) string {
 603     root, sub := util.NormalizeName(pkg)
 604
 605     // For the parent applications source skip the cache.
 606     if root == m.Config.Name {
 607         pth := gpath.Basepath()
 608         return filepath.Join(pth, filepath.FromSlash(sub))
 609     }
 610
 611     d := m.Config.Imports.Get(root)
 612     if d == nil {
 613         d = m.Config.DevImports.Get(root)
 614     }
 615
 616     if d == nil {
 617         d, _ = m.Use.Get(root)
 618
 619         if d == nil {
 620             d = &cfg.Dependency{Name: root}
 621         }
 622     }
 623
 624     key, err := cache.Key(d.Remote())
 625     if err != nil {
 626         msg.Die("Error generating cache key for %s", d.Name)
 627     }
 628
 629     return filepath.Join(cache.Location(), "src", key, filepath.FromSlash(sub))
 630 }

本来、最後629行目で key=https-github.com-golang-protobuf, filePath.FromSlash(sub)=ptypes/dulation にならなければならないが key=https-github.com-golang-protobuf-ptypes-duration になっている。全然ダメだ。 そもそも603行目のutil.NormalizeName()が root=github.com/golang/protobuf/ptypes/duration, sub= を返している。 このライブラリのリポジトリhttps://github.com/golang/protobuf で、リポジトリ内のサブディレクトリは ptypes/dulation なので、ここでは root=github.com/golang/protobuf, sub=ptypes/dulation となってほしい。

ということで util.NoralizeName() を見る。 https://github.com/Masterminds/glide/blob/v0.12.3/util/util.go

300 // NormalizeName takes a package name and normalizes it to the top level package.
301 //
302 // For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is
303 // returned as extra data.
304 //
305 // FIXME: Is this deprecated?
306 func NormalizeName(name string) (string, string) {
307     // Fastpath check if a name in the GOROOT. There is an issue when a pkg
308     // is in the GOROOT and GetRootFromPackage tries to look it up because it
309     // expects remote names.
310     b, err := GetBuildContext()
311     if err == nil {
312         p := filepath.Join(b.GOROOT, "src", name)
313         if _, err := os.Stat(p); err == nil {
314             return toSlash(name), ""
315         }
316     }
317
318     name = toSlash(name)
319     root := GetRootFromPackage(name)
320     extra := strings.TrimPrefix(name, root)
321     if len(extra) > 0 && extra != "/" {
322         extra = strings.TrimPrefix(extra, "/")
323     } else {
324         // If extra is / (which is what it would be here) we want to return ""
325         extra = ""
326     }
327
328     return root, extra
329 }

嫌な感じのコメントが書いてある。 それはいいとして、312行目でGOROOTから該当のライブラリを探している。 私の場合、 GOROOT=/usr/local/go なので、 /usr/local/go/src/github.com/golang/protobuf/ptypes/duration が存在するかどうかを確認している。実際に見てみると、あった。なんであるんじゃい。

 このファイル(ディレクトリ)が存在することで314行目でreturnしてしまって、最終的におかしなURLにライブラリの更新確認に行って死んでいた模様。この後にパスからリポジトリの種類を判別してURLを作るのだがそもそも/usr/local以下はgitはgitでもbrewの配下である。

% sudo rm -rf /usr/local/go/src/github.com

で事なきを得た。そもそもなんでこんなところにこんなものが入っていたのかよくわからない。

 このあたりを探っているとGoogle内部のリポジトリやパッケージ管理法とGitとの相性の悪さで外の人たちが割を食ってる感じがしてちょっと面白い。自分が悩む方でなければ。

 あとGoは読むのが楽で助かる。