猫の魔法

主にruby系の技術メモを記載

railsの起動プロセスを追う その1

はじめに

運良くruby/rails を使うweb系企業に転職して気がつけばもう1年。 色々あったけど、やっぱり私はrubyが好きです(by キキ)

しばらくブログを放置していましたが、せっかくインプットしたことをアウトプットしないのは勿体無いなぁと思ったので、 今回から何回かrails s してから実際サーバが立ち上がるまでどういう風にrailsのコードが実行されるのか雑にソースを追ってみようと思っています。

ゴールとしては Rails.application を rackに突っ込んで、アプリケーションサーバが起動出来るところまで追えればいいかなと考えています。 言うは簡単ですが、これをやるのは色々と周辺知識(rackとか、rackとか、rackとか)が必要になるので、そこも合わせて読んでいけるといいなと考えています。

ただ本当に公私共々忙しいので、更新速度は亀みたいになりそうです。。。もしこのブログを読んでくれる人がいたなら申し訳ないです。

rails s事始め

railsコマンド

まずは作ったrailsアプリのbin/railsを覗いてみます。

#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

APP_PATH以前はとりあえず飛ばすとして、次の関心事はrequire_relative '../config/boot' ですが、これもbundlerのrequireだったりキャッシュのロードだったりで起動には直接関係ないです。 ということでrequire 'rails/commands'を見て行きます。

コマンド処理への道のり

キッカー

Rails::Command.invoke command, ARGV

コマンドを実行するためのクラスの特定

https://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/command.rb#L32 Command.invoke で注目すべきはfind_by_namespace で指定されたコマンドのサブクラスのクラス名を持ってきている部分と、 そのクラス名を使ってperform メソッドを呼んでいるところです。

def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
     command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
   end
end

とりあえず、find_by_namespace を見ていみるとnamespacecommand_name を使ってlookups というところに文字列を入れていて、 これを lookup というメソッドに渡して、何故かsubclasses メソッドからnamespaces というのを引いています。

def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

パット見分からないのですが、 lookup(lookups) では一致するコマンドのrb をrequir していて、serverの場合はhttps://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/commands/server/server_command.rbをrequireしています。ServerCommand クラスは Command のサブクラスなので、 subclasses で引いて来れるんですね。よく出来ています。

namespaces と言っているのは複数の候補が見つかった場合にそれを格納するので複数形になっていますが、とりあえずは気にしなくて良さそうです。 今回はserver を指定しているのでRails::Command::ServerCommandが返る形になります(はず。たぶん。)

performの闇

lib/rails/commands.rb に戻って、find_by_namespace で取得したクラスに対して、以下でperform メソッドを呼んでいます。 https://github.com/rails/rails/blob/5-2-stable/railties/lib/rails/command.rb#L46

こっからが鬼のようにややこしいのですが、perform メソッドは特異クラスのメソッドになります。つまり Rails::Command::ServerCommand の特異クラスの perform メソッドが呼ばれるのですが、Rails::Command::ServerCommandの定義を見ると perform メソッドはインスタンスメソッドの定義しかないのです。 つまりどっかに特異クラスの perform メソッドが定義されているはずで、探してみるとsuperクラスである Rails::Command::Base に定義があります。 rails/base.rb at 5-2-stable · rails/rails · GitHub

 def perform(command, args, config) # :nodoc:
   if Rails::Command::HELP_MAPPINGS.include?(args.first)
     command, args = "help", []
   end

   dispatch(command, args.dup, nil, config)
end

これがRails::Command::ServerCommandインスタンス化をしているのと思いきや、このメソッドは dispatch メソッドを呼び出して終わります。

では dispatch メソッドは何処に?という話になると思うのですが、これは継承元のThordispatch メソッドでした。 このメソッドでは自分自身、つまりRails::Command::ServerCommandnewしてインスタンスを作成しています。 thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub

dispatch メソッドはちょっと長いので、前半部分と後半部分に分けて見ていきます。 まずは前半部分です。

    def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength
      meth ||= retrieve_command_name(given_args)
      command = all_commands[normalize_command_name(meth)]

retrieve_command_name では given_argsの最初の値を持ってきています。こいつにはserverが入っているはずなので、meth の内容はserver という文字列になります。

thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub

    # Retrieve the command name from given args.
    def retrieve_command_name(args) #:nodoc:
      meth = args.first.to_s unless args.empty?
      args.shift if meth && (map[meth] || meth !~ /^\-/)
    end

話をdispatchに戻して続きを見ていきます。 all_commands は 下で解説する method_add で呼ばれる created_command 内で生成されていて meth と一致するThor::Commandインスタンスを返します。 今回の場合は、meth.nameserver になるようなインスタンスを返す形になります。

次に後半を見ていきます。 thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub

      if !command && config[:invoked_via_subcommand]
        # We're a subcommand and our first argument didn't match any of our
        # commands. So we put it back and call our default command.
        given_args.unshift(meth)
        command = all_commands[normalize_command_name(default_command)]
      end

      if command
        args, opts = Thor::Options.split(given_args)
        if stop_on_unknown_option?(command) && !args.empty?
          # given_args starts with a non-option, so we treat everything as
          # ordinary arguments
          args.concat opts
          opts.clear
        end
      else
        args = given_args
        opts = nil
        command = dynamic_command_class.new(meth)
      end

      opts = given_opts || opts || []
      config[:current_command] = command
      config[:command_options] = command.options

      instance = new(args, opts, config)
      yield instance if block_given?
      args = instance.args
      trailing = args[Range.new(arguments.size, -1)]
      instance.invoke_command(command, trailing || [])

!command && config[:invoked_via_subcommand] の部分と if command 〜 else の部分はコマンドが見つからなかった場合の処理と、 オプションが見つからなかった場合の処理なので、今回は関係ありません。 instance = new(args, opts, config)new しているのは自分自身、つまりRails::Command::ServerCommandです。 今回はblock は取っていないので、yield instance if block_given? は関係ありません。 args[Range.new(arguments.size, -1)] は serverコマンドにオプションを渡したときに必要になってくるようですが、 ちょっと今回は置いておきます(もう考えることが手一杯でめんどくさい。。。)

その後invoke_command メソッドを呼び出して何かしらしているのですが、ここからがまた魔境です。

invoke_commandcommandrun しているだけです。 このとき渡しているselfRails::Command::ServerCommandインスタンスですね。 thor/invocation.rb at v0.20.0 · erikhuda/thor · GitHub

def invoke_command(command, *args) #:nodoc:
  current = @_invocations[self.class]

  unless current.include?(command.name)
    current << command.name
    command.run(self, *args)
  end
end

run の中身はとりあえず public_method の場合を見ればいいんですが、この処理ではinstancename メソッドを実行しています。

thor/command.rb at v0.20.0 · erikhuda/thor · GitHub

    def run(instance, args = [])
      arity = nil

      if private_method?(instance)
        instance.class.handle_no_command_error(name)
      elsif public_method?(instance)
        arity = instance.method(name).arity
        instance.__send__(name, *args)
      elsif local_method?(instance, :method_missing)
        instance.__send__(:method_missing, name.to_sym, *args)
      else
        instance.class.handle_no_command_error(name)
      end
    rescue ArgumentError => e
      handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
    rescue NoMethodError => e
      handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (raise e)
    end

このname は 後段で出てくるmethod_added 中で呼ばれる create_commandで生成されています。

thor/thor.rb at v0.20.0 · erikhuda/thor · GitHub

def create_command(meth) #:nodoc:
  @usage ||= nil
  @desc ||= nil
  @long_desc ||= nil
  @hide ||= nil

  if @usage && @desc
    base_class = @hide ? Thor::HiddenCommand : Thor::Command
    commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
    @usage, @desc, @long_desc, @method_options, @hide = nil
    true
   elsif all_commands[meth] || meth == "method_missing"

このときname には server という文字列が入ってきます。 thor/command.rb at v0.20.0 · erikhuda/thor · GitHub

  class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
    FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/

    def initialize(name, description, long_description, usage, options = nil)
      super(name.to_s, description, long_description, usage, options || {})
    end

となると、selfRails::Command::ServerCommandインスタンスなので、 server メソッドがRails::Command::ServerCommandの何処かに定義してあるはずなんです。 はずなんですが。。。どこを見ても見当たらないんですね。

serverとperformの関係

ここまで来ると目だけで追うのは難しい気がしてきたので、binding.pry して、server メソッドのsource_location を見てみたのですが、 何故かRails::Command::ServerCommandperform メソッドを指しているではありませんか。 performメソッドの自体は ServerCommand クラスの下記部分になります。 rails/server_command.rb at 5-2-stable · rails/rails · GitHub

名前が違う物を指すということは、考えられることはalias_method しているのかなと思い調べてみると、railties/lib/rails/command/base.rb で怪しげな処理を見つけました。 alias_method command_name, meth の部分が怪しげです。 rails/base.rb at 5-2-stable · rails/rails · GitHub

          def create_command(meth)
            if meth == "perform"
              alias_method command_name, meth
            else
              # Prevent exception about command without usage.
              # Some commands define their documentation differently.
              @usage ||= ""
              @desc  ||= ""

              super
            end
          end

なんですが、今度はこのメソッドを呼び出しているところがなかなか見つからないんですね。 怪しいところをくまなく探すと、今度はThorの方に怪しげな箇所を見つけました。

method_added というのはメソッドが定義された際に呼ばれるrubyのコールバックです。 つまり、def hogehoge が読み込まれた時点でコールバックとして呼ばれます。 meth には定義されたメソッドの名前が入ります。

thor/base.rb at v0.20.0 · erikhuda/thor · GitHub

      # Fire this callback whenever a method is added. Added methods are
      # tracked as commands by invoking the create_command method.
      def method_added(meth)
        meth = meth.to_s

        if meth == "initialize"
          initialize_added
          return
        end

        # Return if it's not a public instance method
        return unless public_method_defined?(meth.to_sym)

        @no_commands ||= false
        return if @no_commands || !create_command(meth)

        is_thor_reserved_word?(meth, :command)
        Thor::Base.register_klass_file(self)
      end

ここで、methperformが入ってきた場合のことを考えると、create_command ではalias_method command_name, meth が実行されます。 このときcommand_nameserver になっています。

rails/base.rb at 5-2-stable · rails/rails · GitHub

        # Return command name without namespaces.
        #
        #   Rails::Command::TestCommand.command_name # => 'test'
        def command_name
          @command_name ||= begin
            if command = name.to_s.split("::").last
              command.chomp!("Command")
              command.underscore
            end
          end
        end

つまり、perform メソッドは server メソッドとしても存在することになります。 これで、thor/command.rbinstance.__send__(name, *args) で、nameserver の際にRails::Command::ServerCommand#performが呼ばれることの謎が解けました。

継承が重なると定義を探すのが大変ですね。 とりあえず切りがいいので今回はここまでです。

まとめ

  • rails コマンド自体の処理は薄い
  • rails s した後の処理はCommand.invoke 内で行っている
  • コマンドを目的のクラス(Rails::Command::ServerCommand)にdispatchする処理はrails以外にthorも複雑に絡んでいる。
  • aliasメソッドを使って、performメソッドをserver` メソッドとして呼び出せるようにしている。

次回はperformメソッドから見て行きます。

rubyソースコードリーディング 1日目

特に理由はないが、読める所からrubyのソースを読もうと思う。 パット見途中で挫折しそうだが、何も無しに何処まで読めるか試してみるのもありかなと。

とりあえずはv2_5_0_rc1のmain.cから

int
main(int argc, char **argv)
{
#ifdef RUBY_DEBUG_ENV
    ruby_set_debug_option(getenv("RUBY_DEBUG"));
#endif

RUBY_DEBUG_ENVが有効ならruby_set_debug_optionを呼ぶ。

RUBY_DEBUG_ENVはRUBY_DEVELが有効であれば定義される。 

#if RUBY_DEVEL && !defined RUBY_DEBUG_ENV
# define RUBY_DEBUG_ENV 1
#endif

RUBY_DEVELはconfigure.acで定義されているか、こっちはm4マクロなので読み解くのがしんどい。 恐らく、RUBY_PATCHLEVELに入っている物のうち、-*に一致する場合はRUBY_DEVELがyesに、そうでない場合はnoが入るということだと思うが、 なんでそういうことをしているかはよく分からない。

AS_CASE([$RUBY_PATCHLEVEL], [-*],
    [RUBY_DEVEL=yes], [RUBY_DEVEL=no])

戻って、ruby_set_debug_optionはdebug.cで定義されていて、中でruby_each_wordsを呼んでいる。

void
ruby_set_debug_option(const char *str)
{
    ruby_each_words(str, set_debug_option, 0);
}

set_debug_optionはdebug.c上でstatic voidとして定義されているので第二引数は関数ポインタと思われる(こう言う書き方が出来るは知らなかった)

static void
set_debug_option(const char *str, int len, void *arg)
{
    if (!ruby_env_debug_option(str, len, arg)) {
    fprintf(stderr, "unexpected debug option: %.*s\n", len, str);
    }
}

とりあえず今日はここまで。

binding.pry チートシート

binding.pry使ってデバックを行う時のチートシートが欲しかったので自分で作ることにした。随時更新。

チートシート

pry関連リンク

rails以外

rails

pryの使い方

Gemfileに必要なgemを追加した上で、 実行を止めて状態を見たい場所に以下を挿入。

binding.pry

閲覧系

コマンド 説明 gem
$ ソースを表示
$ オブジェクト名 オブジェクト名のソースを表示
ls カレントにあるメソッドやオブジェクトを表示
show-stack スタックトレースを表示 pry-stack_explorer

実行系

コマンド 説明
step ステップ実行(メソッドが呼ばれる場合はその中に入っていく)
next 次の行を実行(メソッドが呼ばれても中には入らない
exit 現在のpryを終了(もう一度binding.pryが呼ばれると止まる
!!! プログラムを強制終了

更新系

コマンド 説明
edit ファイルパス ファイルパスのファイルを編集(一時的ではなく恒久的に編集される点に注意)
edit -c 現在のファイルを編集

brewコマンドをrakeタスクの中から呼ぶ方法

Rakefileを使ってbrewコマンドラインから叩いて実行するスクリプトを作っていたのだが、エラーが出て上手く行かなかった。 ネットを結構探したが、解決策がなかなか見つからなかったのでここに顛末を残しておく。

経緯

Rakefileを作って、その中からshでbrewを読んでみた所、以下のエラーが出てうまく動作しなかった。 なお、bundle execしているのはGemfileを使っている為。

Rakefile

require 'pry'

task zsh_install do
  sh(`brew install zsh`)
end

実行コマンド

$ bundle exec rake zsh_install

エラー

Could not find rake-12.1.0 in any of the sources
Run `bundle install` to install missing gems.

エラーに書いてある通り、bundle installしたり、環境変数をいじってみたりしたがどうにも上手く行かなかった。
とりあえずrequire 'pry'を一時的に消して、bundle execを外して実行して見たところ、上手く動く事が判明した。

結論

で、なんでという話になるのだが、bundelrのdocページにHomebrewをbundleで実行するスクリプトの中から叩く場合はBunder.with_clean_envのブロック内でやれよ的な事が書いてあった。 つまりbundler対応させるのであれば、やり方が違うという話のようだ。

念の為、ソースを見て裏を取っていたら、どうやらwith_clean_envが使っているclean_envはdeprecatedで、 代わりにoriginal_env使ってね的な事が書いてあったので、 以下のようにソースを修正して上手く動く事を確認した。

require 'pry'

task zsh_install do
  Bundler.with_original_env do
    sh(`brew install zsh`)
  end
end
 

with_clean_envとwith_original_env

bundlerの話はソースを信じて、original_envの方を使ったが bundlerのドキュメントの方はwith_clean_envを使う方法が書いてある。
また、clean_envがdeprecatedになったのは2年以上前のようでその後の進捗がないのも気になる。
PR内の話を信じるなら、bundle 2.0ではclean_envをサポートしないって言っているけど、2.0のコードを見るとまだdeprecatedで残っているので、何がホントなのかよく分からない。。。

今日の学び 2017/10/17

だいぶブログを休んでいたが、やっと生活が落ち着いて来たので学んだ事があったら書き留めて行こうと思う。 空白期間にあった話は、気が向けば今度記事にしようと思う。

とりあえず平日はrubyvim関連で個人的にメモって置いた方が良さそうなことを記載して行こうと思う。基本的にコード書く時間を優先するので、毎日更新はしないかもだけど。。。。

※気になる所あったらツッコミ歓迎です。

正規表現の比較

比較だけしたいなら===で比較する。正規表現側が左辺値になるので注意

str = 'hoge'

/hoge/ === str 
=>true
/wan/ === str 
=>false

mapでハッシュのキー値を StringからSymbolに

ymlファイルからデータ読み込むと、キー値が文字列のハッシュで読み込まれるので、それをsymbolに変えたいと思った事がきっかけ。簡単にやるならmapするのがいい。

sample = {"hoge"=>"neko"}
sample.map {|key, value|  [key.to_sim, value] }.to_h
=>{:hoge=>"neko"}

上の説明

mapはレシーバーをブロック内で展開し、その評価結果を1要素として配列を作って返す。それをto_hするとハッシュに戻る仕組み。

gsubの謎

Codewarsでよく分からない回答があったので、自分なりに調べてみた。

問題はカード番号のマスキングのように下4桁以外の文字なり数字を#でマスキングするという物で、 自分は以下のような回答で提出した(色々褒められた物ではないが。。。)

def maskify(cc)
  cc.to_s[0, cc.length-4] = "#"*(cc.length-4) if cc.length > 4
  cc
end

実際に他の人の回答を見てみると、以下の回答に支持が集まっていた。

def maskify(cc)
  cc.gsub(/.(?=....)/, '#')
end

確かにシンプルですごいと思ったのだが、
どうしてこれで条件を満たすのかパット見分からなかったので調べてみた。

調査

まずgusbだが、 これは正規表現で一致した文字列を第二引数の文字に変換した文字列を返すという物である。

問題は正規表現の方だが、 .は任意の1文字の文字列、?=は肯定先読みという物で、正規表現の一致した部分の前の部分を一致として返す物になる。

この説明だとよく分からないので、肯定先読みの具体例を出すと、hogehの場合、ogehが(?=....)と一致するので、hが一致した部分として返される事になる。
hogehogehogeの場合はhoge(?=....)と一致するのでhogehogeの最後のeが一致した部分として返される。
ちなみにhogeの場合は(?=....)と一致するが、直前の文字はないのでrubyでは空が返される。

.(?=…)と.*(?=…)の違い

今回は一致した文字の前の文字列も欲しいので、.*(?=....)という風にするのが、一致させるという意味では正しいと自分は思っていた。
しかし、gsub(/.*(?=....)/,'#')だとhogehogehogeの場合、hogehogeが一致している事になり、これが#と置換されてしまう。
本来であればhogehogeを########と置換して欲しいので、このやり方は上手く行かない。

対して、gsub(/.(?=...)/,'#')の場合、hogehogehogeは
1回目:hがogehと一致する為一致
2回目:oがgehoと一致する為一致
3回目:gがehogと一致する為一致
・・・
8回目:eがhogeと一致する為一致
9回目:hはその後ろに任意の4文字がないので一致しない

という風に動き、1〜8回目それぞれのタイミングで文字が#と置換される事が実際に試して見て分かった。
つまりgsubは一致した項目を最後まで次々と処理して第二引数の物に置き換えて行くことになり、今回の処理が上手く動くことになるようだ。

ここまで分かって、あの問題を解く人達は素直にすごいと思った。

プログラミング能力を鍛える手段

今日は毛色を変えて、自分が使っているプログラミング能力を鍛える方法を紹介して見る

自分の場合、普段プログラムを組まないので、出来るだけ組む時間を取りたいと思っている。
だからと言って、何かアプリケーションを作るとなると、環境設定や設計に時間が取られ、 組む前に貴重な時間を食いつぶしてしまう事も珍しくない。

そこで、自分はコーディングの練習用として、codewarsというサービスを使っている。

www.codewars.com

codewarsはオンラインで出来るプログラミング学習用SNSで、言語を問わず色々な課題が公開されていて、無料で課題を試す事が出来る。

プログラミングの課題は、KATAというプログラム課題郡の中から適当な課題を選び、設問者が用意したテストをすべて通るようにプログラムを組む形で行っていく事になる。
組んだ後は他の人の回答も見れる為、他の人がどういう風に問題を解くかというのを見れるという意味でも為になる物となっている。
他に、KUMITEという、よりよいコードを勝負し合うという物があるが、自分はKATAの方しか使っていない。

基本英語だが、いい問題も多いのでとりあえずgoogle翻訳掛けながらでもやる価値はある。

自分の場合、あまり難易度が高い物だとこれもそもそも問題を理解するのに時間が掛かるので、
喋れるように練習するという意味ではまずは難易度が低い物を何個も試してみている。

興味のある方は是非。

[個人的メモ]Webを支える技術(その1)

公私共々忙し過ぎて、久々の更新。
少なくとも週1更新位のペースは維持したいが、どうなるやら…

少し前に「Webを支える技術」を買って読んだ。

普段何気なく使っているHTTPやHTMLと言ったWebの技術を、
特定のプログラム言語ではなく、その技術にフォーカスして学べるという意味ではとてもいい本だった。
忘れないように読んだ内容を記録しておく。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

2.6章 SOAPとRESTの歴史

Web系の話は大学時代に少し学んだ程度だったので、何故現在RESTが主流になったのかが記載されていたのはとても参考になった。
SOAPがRESTの対立項だったというのは知らなかった。

7.11章 べき等性と安全性

すぐ分からなくなる「べき等」という言葉。
「ある操作を何回行っても結果が同じこと」という説明がとてもしっくり来た。

また、メソッド毎にべき等と安全についての記載があるのは良かった。
下記の区分分けはとても分かり易かった。

  • べき等で安全:GET,HEAD
  • べき等で安全でない:PUT,DELETE
  • べき等でもなく安全でもない:POST

7.12章 PUTがべき等でなくなる例

PUTでべき等でなくなる例と、PUTでべき等である例が記載されていて、なるほどと思った。

PUT /pen/price HTTP/1.1
Host: hoge.jp
Content-Type: text/plain;

100

上記でペンの値段が+100円されるのであれば、もう一度同じリクエストを投げるとペンの値段がさらに+100円される事になる。
これは、結果が変わってしまう事になるので、べき等ではない。

逆に、上記でペンの値段が100円になるのであれば、もう一度同じリクエストを投げてもペンの値段は100円のままになる。
この場合、結果が変わらない為、べき等になる。

つまり、べき等かどうかはHTTPリクエストのそれのみで決まるのではなく、HTTPリクエストを受けた側がどう解釈するかが重要になる。

今日はここまで

再々考:classとそれが読み込んでいるmoduleの一覧を表示する方法(bugfix)

nekomaho.hatenablog.jp

前に上記の記事を書いたが、改めて見直して見た所、新メソッドと旧メソッドの比較が全く意味の無い物となっている事が分かった。

※記事のアップ時間が夜中の2時な事を差し引いてもちょっと酷いと思う。

コードを再掲するので、モノ好きな方は何が間違っているのか見て見て欲しい。
ちなみに出力は間違っていない。
答えは下に掲載している。

def display_class_modules_old(class_name)
  print "class:#{class_name},modules:#{class_name.included_modules}","\n" if class_name.respond_to?(:included_modules)
  display_class_modules(class_name.superclass) if class_name.respond_to?(:superclass)
end


def display_class_modules(class_name)
  class_name.ancestors.each do |klass|
     puts "class:#{klass},modules:#{klass.included_modules}" if klass.is_a?(Class)
  end
end

puts 'same' if display_class_modules(1.class) == display_class_modules(1.class)


#=> class:Integer,modules:[Comparable, Kernel]
#=> class:Numeric,modules:[Comparable, Kernel]
#=> class:Object,modules:[Kernel]
#=> class:BasicObject,modules:[]
#=> class:Integer,modules:[Comparable, Kernel]
#=> class:Numeric,modules:[Comparable, Kernel]
#=> class:Object,modules:[Kernel]
#=> class:BasicObject,modules:[]
#=> same












間違い

間違いは以下の3つ。

1: 同じかどうかの比較をしている物がおかしい

puts 'same' if display_class_modules(1.class) == display_class_modules(1.class)

上記、ifの左辺値と右辺値がスペルミスで同じメソッドを比較している。それは同じ結果になるだろう。

2: oldがちゃんとoldのメソッドを呼んでいない

仮に1を直すと、display_class_modulesとdisplay_class_modules_oldは異なる値となる。 何故なら、これもタイプミスだがdisplay_class_modules_oldが再帰的に呼んでいるメソッドがdisplay_class_modulesだからだ。
※こう言うときはaliasを使うのが正しいのでしょう。。。

3: そもそも比較出来ていない

仮に1と2を直したとしても、1で行っているようなメソッド同時を==で比較する方法では正しく比較が出来ない。
なぜなら、この2つのメソッドは返している値が異なるからだ。
新しい方はancestorsの戻りが返るが、古い方はputsしているだけなのでnilが返る。
本来なら、何かしらの戻りを比較するか、putsで出している内容を比較する必要がある。

1〜3を統合して、直したのが以下のロジックになる。

def display_class_modules_old(class_name)
  calls ||= []
  calls = [{class: class_name, modules: class_name.included_modules}] if class_name.respond_to?(:included_modules)
  calls << display_class_modules_old(class_name.superclass) if class_name.respond_to?(:superclass)
  calls.flatten
end

def display_class_modules(class_name)
  calls ||= []
  class_name.ancestors.each do |klass|
     calls << {class: klass,modules: klass.included_modules} if klass.is_a?(Class)
  end
  calls
end

old=display_class_modules_old(1.class)
new=display_class_modules(1.class)

old == new ? puts("same") : puts("different")

#=> same

これで本当に正しいのか。。。何か気がついた方居ましたら教えて下さい。

ちなみに自分は両方のメソッドの中身を書き換えたが、
恐らくメソッドの中身を直接書き換える事なく比較する方法もある。
これは時間あるときにチャレンジしてみたい。

railsアプリケーションを作った

しばらくぶりの更新。 仕事の忙しさとアプリケーション作成に時間を取られて、随分ご無沙汰になってしまった。

今後は今まで通り、こっちに割く時間も取ろうと思う。

とりあえず作っていたrailsアプリケーションがある程度公開出来る物になったので、このブログにも貼っておこうと思う。

GitHub - nekomaho/task-controller

task-controllerは簡易なタスク管理を行う用と思って作ったもの。

本当はクリティカルパスを自動的に求めてくれるような物を作ろうとしたが、 時間(一週間で取れる時間がいい時で8h程度だったので、とりあえずアウトプットを出した感じ)と技術力の点で一旦タスクを登録して関連付け(これも条件付きで、やりたいことは出来ていない)だけ出来るような物を作成した

公開した物については、まだアルファ版な上、やり残しも大量にあるのでちょっとずつ消化していけたらいいなと思っている。