Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: camelCase🐫を使ってRubyを書くことは出来そうなのか?色々試してみたのでMEMO🤔

フロントエンドでJavaScriptを採用してバックエンドをRubyで書いていると、snake_case🐍とcamelCase🐫を行ったり来たりして、混乱したりするのでRubyでcamelCase🐫で書けたりしないのか、ちょっと試してみたのでMEMO📝

記事内のコードはこちらにあげています🐫💎

github.com

やりたいこと

自分で生やすメソッドに関しては単純にcamelCase🐫で書けばいいだけなんですが、

class Book
  def initialize(name, publishedAt)
    @name = name
    @publishedAt = publishedAt
  end

  attr_reader :name, :publishedAt
end

book = Book.new('foo', Time.now)
p book.name
p book.publishedAt

ネックなのは組み込みライブラリのメソッドはsnake_case🐍なので、単純に書くと🐍と🐫が混在してしまうのが辛いところですね😓

class Book
  def initialize(name, publishedAt)
    @name = name
    @publishedAt = publishedAt
  end

  attr_reader :name, :publishedAt
end

book = Book.new('foo', Time.now)
p book.name
p book.publishedAt.to_s

標準ライブラリのメソッドも含めてcamelCase🐫で書けないかと思った次第です。

ちなみにrubocopでcamelCaseのstyleを矯正することもできます👮

github.com

GithubでcamelCaseのルールを採用しているコードが無いか、検索してみたところ殆どないけど、全く無いわけではないっぽい👀

https://github.com/search?l=YAML&q=EnforcedStyle%3A+camelCase&type=Code

method_missingを使って無理やり🐫で書いてみる

以下のようなmethod_missingをオーバーライドして、method.to_s.underscore.to_symを実行して🐫→🐍してからpublic_sendしてメソッドを実行するようなActiveSupport::Concernをextendしたmodule CameRubyを用意してみました。

require 'active_support/all'

module CameRuby
  module MethodMissing
    extend ActiveSupport::Concern
    module ClassMethods
      def method_missing(method, *args)
        snaky_name = method.to_s.underscore.to_sym
        if public_methods.include?(snaky_name)
          public_send(snaky_name, *args)
        else
          super
        end
      end

      def respond_to_missing?(method, include_private)
        snaky_name = method.to_s.underscore.to_sym
        public_methods.include?(snaky_name) ? true : super
      end
    end

    def method_missing(method, *args)
      snaky_name = method.to_s.underscore.to_sym
      if public_methods.include?(snaky_name)
        self.public_send(snaky_name, *args)
      else
        super
      end
    end


    def respond_to_missing?(method, include_private)
      snaky_name = method.to_s.underscore.to_sym
      public_methods.include?(snaky_name) ? true : super
    end
  end
end

これをObjectクラスにincludeして使うと以下のように🐫で書くことが出来ます。

require_relative './lib/cameRuby/methodMissing'
class Object
  include CameRuby::MethodMissing
end

class Book
  def initialize(name, publishedAt)
    @name = name
    @publishedAt = publishedAt
  end

  attr_reader :name, :publishedAt
end

book = Book.new('foo', Time.now)
p book.name
#=> "foo"
p book.publishedAt.toS
#=> "2020-11-14 14:00:54 +0900"

alias_methodを使って🐫でも呼び出し可能にする

以下のようなObjectSpace.each_object(Class)を使ってClassを継承している全Objectを取得し、method_name.to_s.camelize(:lower).to_symを実行して🐍→🐫をしてからalias_methodで🐫で呼び出し可能にするような処理をCameRuby::AliasMethod.callで呼び出せるようにして、

# fornzen_string_literal: true

require 'active_support/all'

module CameRuby
  class AliasMethod
    def self.call
      ObjectSpace.each_object(Class) do |klass|
        klass.instance_methods.each do |method_name|
          camelized_name = method_name.to_s.camelize(:lower).to_sym
          next if camelized_name == method_name

          klass.instance_eval do
            alias_method(camelized_name, method_name)
          end
        rescue FrozenError => err
          next
        end
      end
    end
  end
end

以下のように呼び出すと、🐫で書くことが出来ます。

require_relative './lib/cameRuby/aliasMethod'
CameRuby::AliasMethod.call

puts 'foo'.respond_to?(:toS, false)
puts 'foo'.toS
puts String.respond_to?(:tryConvert, false)
puts String.tryConvert('str')

class Book
  def initialize(name, publishedAt)
    @name = name
    @publishedAt = publishedAt
  end

  attr_reader :name, :publishedAt
end

book = Book.new('foo', Time.now)
p book.name
#=> "foo"
p book.publishedAt.toS
#=> "2020-11-14 14:23:24 +0900"

ベンチマーク

method_missingを使う方法

大分パフォーマンスへの影響が大きい😓

require_relative './lib/cameRuby/methodMissing'
class Object
  include CameRuby::MethodMissing
end

n = 100000
Benchmark.bm(7, ">total:", ">avg:") do |x|
  x.report("to_s:") { "foo".to_s }
  x.report("toS:") { "foo".toS }
end

              user     system      total        real
to_s:     0.000008   0.000007   0.000015 (  0.000008)
toS:      0.000131   0.000001   0.000132 (  0.000132)

alias_methodを使う方法

意外と速い🤔(けど、メモリ上の全Classの各メソッドにalias_methodを実行するのでその時間は掛かる)

require_relative './lib/cameRuby/aliasMethod'
CameRuby::AliasMethod.call

n = 100000
Benchmark.bm(7, ">total:", ">avg:") do |x|
  x.report("to_s:") { "foo".to_s }
  x.report("toS:") { "foo".toS }
end

              user     system      total        real
to_s:     0.000009   0.000006   0.000015 (  0.000008)
toS:      0.000003   0.000001   0.000004 (  0.000002)

おわりに

軽く試してみた感じ🐫で書けそうな気配を感じましたが、大分パフォーマンスへの影響が大きく、method_missing or instance_eval,alias_methodで割と無理やりな感じになっているので、本番運用に耐えられそうにはないですし、使えたとしても趣味レベルな感じですね😅

あとはIDEのインテリセンスとかもかなり影響を受けそうなので、結果素直に🐍で書いたほうが良いということがわかった。。。

参考

qiita.com

docs.ruby-lang.org