Rubyの将来のバージョンから欲しいもの、そして今どのように対処しているか

こんにちは、Habr。

私は約1年間Rubyで仕事をしてきましたが、私が個人的に見逃しがちなもの、および言語に埋め込まれたものを書きたいと思っています。 おそらく、これらのポイントのうちの2つだけが本当に深刻な欠陥であり、残りは即興の手段で簡単に処理できます。

それは欠陥と些細なことのようですが、それらは作業を著しく複雑にします-補助メソッドの独自のライブラリを書かなければなりません。 また、他の人のコードを開くこともありますが、そこにはあなたとまったく同じ補助機能があります。 これは、私が思うに、標準言語ライブラリが不完全であることの表れです。 開発者の1人がテキストを読み、パッチをコミットすることを期待しましょう。 ;-)

それでは、順番に始めましょう:

C ++のように、異なる引数リストを使用したメソッドのオーバーロード



私の意見では、これは切断の最大の問題の1つです。 引数のタイプごとにメソッドを異なる方法で定義することはできません。 その結果、パラメータのリストを解析するための複雑な構造、チェックタイプ、引数の有無などを記述する必要があります。 私はこの問題を解決するふりをしません。それを解決するには、ソースをシャベルで掘り、多くのコードとさらに多くのテストを書く必要があります。 したがって、最も単純な場合に数行のコードを節約できる実装のみを示します。

このために、alias-method-chain'ingを使用します。

module MethodOverload module ClassMethods def overload(meth, submethods_hash) if method_defined?(meth) alias_method "#{meth}_default_behavior", meth has_default_behavior = true end define_method meth do |*args,&block| submethods_hash.each do |sub_meth,arg_typelist| if args.size == arg_typelist.size && args.to_enum.with_index.all?{|arg,index| arg.is_a? arg_typelist[index]} return self.send(sub_meth,*args,&block) end end if has_default_behavior return self.send("#{meth}_default_behavior",*args,&block) else raise ArgumentError end end end end def self.included(base) base.extend(ClassMethods) end end
      
      







次の結果を生成するような関数を作成します。

  x = X.new xf # => "original version []" xf 3, 'a' # => "original version [3,\"a\"]" xf 3, 4 # => "pair numeric version 3 & 4" xf 3 # => "numeric version 3" xf 'mystr' # => "string version mystr"
      
      







メソッドのオーバーロードをシミュレートすると、次のようになります。

  class X include MethodOverload def f(*args) 'original version ' + args.to_s end def f_string(str) 'string version ' + str.to_s end def f_numeric(num) 'numeric version ' + num.to_s end def f_pair_numeric(num1,num2) "pair numeric version #{num1} & #{num2}" end overload :f, f_string:[String], f_numeric:[Numeric], f_pair_numeric:[Numeric, Numeric] end
      
      







代わりに、もちろん、単に1つの関数を書くことができます

  class X def f(*args) if args.size == 2 && args.first.is_a? Numeric && args.last.is_a? Numeric "pair numeric version #{args[0]} #{args[1]}" elsif args.size == 1 && args.first.is_a? String 'string version ' + str.to_s elsif args.size == 1 && args.first.is_a? Numeric 'numeric version ' + num.to_s else 'original version ' + args.to_s end end end
      
      







ただし、このような方法は非常に急速に膨張し、機能の読み取りや追加には不便になります。 残念ながら、私が説明したアプローチには、デフォルトの引数やリストの圧縮(* args)の操作などの問題がまだありますが、オーバーロードメソッドを少し拡張することで解決できると思います。 コミュニティにとって、このようなアプローチを開発するのが良いと思われる場合は、このテーマに関する別の記事を作成し、コードを拡張してみます。

Rubyの将来のバージョンでは、このようなメソッドのオーバーロードのための組み込みツールが必要です。



ハッシュを表示して、配列ではなく別のハッシュを取得します



正直なところ、このメソッドがEnumerableに直接含まれていないのはなぜなのか迷っています。これは最も頻繁に必要な構成の1つです。 私は確かに別の方法でそれを持ちたいと思っています。2番目のルビーでさえもそうではありません。

私はしばしば、ハッシュを通過して、それから別のハッシュを作成するタスクを持っています。 一種の地図。 それはハッシュのマップだけで何を提供しますか? そうです、配列。 通常、同様のデザインで抜け出す必要があります。

 {a: 1, b:2, c:3, z:26}.inject({}){|hsh,(k,v)| hsh.merge(k=>v.even?)} # => {a: false, b: true, c:false, z: true}
      
      





もう1つのオプションがあります。
 Hash[{a: 1, b:2, c:3, z:26}.map{|k,v| [k,v.even?]}]
      
      



このオプションは少しシンプルで致命的です 値だけでなくキーも表示できます。 ただし、配列にはHash [arr]ではなくarr.to_hashを書き込むためのto_hashメソッドがないことは明らかです。

ただし、この方法はすべての配列に適用できるわけではありません。おそらくこれらの理由により、Array#to_hashメソッドはカーネルにありません( 説明を参照)。

これは、HashLikeArrayクラスをArrayから派生させ、[[k1、v1]、[k2、v2]、...]という形式の配列のみを受け入れる価値があることを示唆していますが、これについては次のセクションで詳しく説明します。



それまでの間、注入メソッドに基づいて簡単なhash_mapを実装します。

  class Hash def hash_map(&block) inject({}) do |hsh,(k,v)| hsh.merge( k=> block.call(v)) end end end
      
      







そして今、Arrayから派生したクラスの実装について。 これには、解決しようとするいくつかの問題があります。



クラスのインスタンスを独自のサブクラスのインスタンスに変換します



Arrayから派生したクラスを作成しても問題ないことは明らかですが、Hash#to_aメソッドを置き換えてArrayを返さないようにする必要がありますが、この派生クラスHashLikeArrayです。 もちろん、このメソッドの独自の実装を作成することもできますが、実際には、Arrayクラスの元のHash#to_aの結果をHashLikeArrayのサブクラスに変換するラッパーのみが必要です。

書いてみましょう(エイリアスに悩まされることなく)

  class HashLikeArray < Array def initialize(*args,&block) #raise unless ... -    ,        super end end class Hash def to_a_another_version HashLikeArray[*self.to_a] #   ,    Array::[] end end {a:3,b:5,c:8}.to_a.class # ==> Array {a:3,b:5,c:8}.to_a_another_version.class # ==> HashLikeArray
      
      







管理されています。 配列よりも開発されたクラスが少ない場合でも、タスクはより複雑になります。

  class Animal attr_accessor :weight def self.get_flying_animal res = self.new res.singleton_class.class_eval { attr_accessor :flight_velocity} res.flight_velocity = 1 res end end class Bird < Animal attr_accessor :flight_velocity end
      
      





次に、古いget_flying_animalメソッドを使用してみましょう。これは、Birdクラスが存在しないときに作成されました。 Animal :: get_flying_animalは常に属性が付加された鳥を返しましたが、純粋に正式にはそれらはAnimalクラスのものでした。 ここで、Animalクラスを変更せずにBird :: get_flying_animalメソッドを作成します。これは、同じアルゴリズムを使用して同じ鳥を返しますが、現在はBirdクラスのみです。 はい、get_flying_animalメソッドは実際には例よりもはるかに大きいため、複製する必要はありません。

特に、Animalクラスを変更できない場合、またはそのソースさえわからない場合、この状況はあなたの人生を台無しにする可能性があります。 たとえば、siで書かれています。 (演習として、Array :: []またはArray#replaceメソッドを使用せずに、HashLikeArray#to_aメソッドのバージョンを作成してみてください。もちろん、ライブラリを作成しない限り、コードを複製する場所はありません)

すべてのインスタンス変数を派生クラスのオブジェクトにコピーする、エレガントな方法でのみこれを行う方法を見つけました

  class Object def type_cast(new_class) new_obj = new_class.allocate instance_variables.each do |varname| new_obj.instance_variable_set(varname, self.instance_variable_get(varname)) end new_obj end end
      
      







これで次のことができます。

  def Bird.get_flying_animal Animal.get_flying_animal.type_cast(Bird) end
      
      







type_castメソッドの形式でこの厄介な松葉杖を置き換える方法を誰かが知っている場合-書き込み、これは非常に興味深い質問です。

一般的に言って、これはクラス型をサブクラス型に再定義することがどれだけ正しいかという議論の余地のある質問です。 これは汚いハックであり、OOPではありませんが、時には非常に便利なハックであると言えます。Rubyは、各ルールが必要なだけの特別な言語であるため、正確に違反できるものが明確になります。 作成後にオブジェクトを他のクラスに変更して、次のような指示を出すことができる場合、これは切断の原則とはあまり関係がないと思います:

 x=X.new x.class = Y
      
      





結局のところ、クラスは実際にはメソッド、定数、および@@-変数を検索するための領域のみを意味するため、クラスを定義するとき(作成時またはその後)にどのような違いが生じますか。 この時点からオブジェクトの整合性に対する全責任はユーザーにありますが、これは彼の権利です。 さらに、新しいクラスが古いクラスのサブクラスである場合、責任は主に古いクラスに委任されます。



別の小さなもの



さて、ささいなことについて:



自己返却方法


さらに、次のようなメソッドを導入することをお勧めします

  def identity self end
      
      





これは、.collect {| x | |の代わりに.collect(&:identity)のような何かをブロックに書き込むことができるので便利です。 x}

番号付けと収集の場合、すべてが単純に解決されます-to_aメソッドを使用すると、オブジェクトの完全なリストを取得できますが、ブロックを取る独自のメソッドを作成する場合、この関数は便利です。

たとえば、railメソッドNil#tryのようなメソッドを作成しましょ

  class Object def try(method_name,&block) send method_name rescue yield end end
      
      







今、私たちはできる

 x.try(:transform,&:another_transform)
      
      





そして、ここで私たちのアイデンティティーメソッドが必要になるかもしれません:

 x.try(:transform, &:identity)
      
      





-変換が実行できる場合、実行されます、いいえ-元のオブジェクトが返されます。



Object#in?(コレクション)メソッド


レール方式
 color.in?([:red,:green,:blue])
      
      



よりずっと良く見える
 [:red,:green,:blue].include?(color)
      
      





私たちは書きます:

  def in?(collection) raise ArgumentError unless collection.respond_to? :include? collection.include? self end
      
      







この方法のために、ライブラリ全体(ActiveSupportのようです)を接続するのは非常に面倒です。 この方法はカーネル内でうまく機能すると思います。



質問形式の否定形


arr.not_emptyを書きますか? 不器用な!空の代わりに? ルビーには多くの同様の質問方法がありますが、否定はほとんどありません。 not_empty型のメソッド? not_nil? 標準ライブラリに組み込みたいのですが。 method_missingを使用してネガティブな独自の質問メソッドを定義することを好みます(明らかな理由により、Objectクラスのmethod_missingを変更することは推奨されません)

  class X def method_missing(meth,*args,&block) match = meth.to_s.match(/^not_(.+\?)$/) return !send(match[1], *args,&block) if match && respond_to?(match[1]) super end end X.new.not_nil? # => true
      
      







ファイル拡張子を切り捨てる


Fileモジュールには、filename_wo_extnameメソッドとbasename_wo_extnameメソッドがないという嫌悪感があります。 修正します:

  class File def self.filename_wo_ext(filename) filename[0..-(1+self.extname(filename).length)] end def self.basename_wo_ext(filename) self.basename(filename)[0..-(1+self.extname(filename).length)] end end File.basename_wo_ext('d:/my_folder/my_file.txt') # => "my_file" File.filename_wo_ext('d:/my_folder/my_file.txt') # => "d:/my_folder/my_file"
      
      







いくつかの喜び


このような山ほどの苦情を、すべてにかかわらず素晴らしいRuby言語で修正する必要があります。 したがって、ルビーの世界からの良いニュースの1つである遅延分子を共有します。



All Articles