📜 ⬆️ ⬇️

What I would like from future versions of Ruby, and how I cope now

Good afternoon, Habr.
I have been working with Ruby for about a year and would like to write about some things that I personally often lack there, and which I would like to see embedded in the language. Perhaps only a couple of these points are really serious flaws, with the rest you can easily cope with improvised means.
It seems to be flaws and trivialities, but they significantly complicate the work - you have to write your own libraries of auxiliary methods that you can’t select in heme - it’s painfully small and uncomfortable without them. And sometimes you open someone else's code - and you see there exactly the same auxiliary functions as yours. I think this is a sign that the standard language library is incomplete. Well, let's hope, one of the developers will read the text and commit the patch. ;-)
So let's start in order:

Method overloading with different argument lists, as in C ++


This is in my opinion one of the biggest problems of ruby. It does not allow to define a method differently for different types of arguments. As a result, you have to write complex constructions for parsing the list of parameters, check types, the presence or absence of arguments, and so on. I do not pretend to solve this problem, to solve it you need to shovel the source code, write a lot of code and even more tests. Therefore, I will show only an implementation that in the simplest cases can save you a couple of lines of code.
To do this, we will use the 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 


We write a function such that it yields the following results:
  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" 


Here's what it looks like if you simulate method overloading:
  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 

')
Instead, of course, you can simply write one function.
  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 


However, this method swells very quickly and becomes inconvenient for reading and adding features. Unfortunately, in the approach described by me, there are still problems such as working with arguments by default and list compression (* args), but I think they can be solved by slightly expanding the overload method. If it seems to the community that it would be nice to develop such an approach - I will try to make a separate article on this topic and expand the code.
I would like in future versions of Ruby to have built-in tools for this method overload.

Display hash and get another hash from it, not an array


Honestly, I wonder why this method is not directly in Enumerable, it is one of the most frequent constructions that is needed. I certainly would like her to have a separate method, and not even in the second cut, but the sooner the better (especially since this is a trifling matter).
I often have the task of walking through a hash and making another hash of it. A sort of map. Here only map for hash will give you what? Right, array. Usually it is necessary to get out a similar construction:
 {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} 

There is another option
 Hash[{a: 1, b:2, c:3, z:26}.map{|k,v| [k,v.even?]}] 
This option is a bit simpler and more deadly. allows you to display not only values, but also keys. However, it is clear that the arrays lack the to_hash method to write not Hash [arr], but arr.to_hash.
However, this method will not be applicable to all arrays, probably for these reasons the Array # to_hash method is not present in the kernel (see discussion ).
This hints that it is worth making the Array derived class HashLikeArray, which accepts only arrays of the form [[k1, v1], [k2, v2], ...], but this is in the next paragraph.

For now, let's implement a simple hash_map based on the inject method:
  class Hash def hash_map(&block) inject({}) do |hsh,(k,v)| hsh.merge( k=> block.call(v)) end end end 


And now about the implementation of a class derived from Array. There are some problems with this that we will try to solve now.

Convert an instance of a class to an instance of its own subclass.


It is clear that there is no problem to create a class derived from Array, but we also need to somehow replace the Hash # to_a method so that it returns not this Array, but this derived class HashLikeArray. Of course, you can try to write your own implementation of this method, but in reality you just need a wrapper that turns the result of the original Hash # to_a from the Array class into a subclass of HashLikeArray.
Let's try to write (not bothering now with any alias)
  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 


Coped. Now the task is more complicated, even if we have a less developed class than Array:
  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 

Now let you have an old get_flying_animal method that was written when the Bird class did not exist yet. Animal :: get_flying_animal always returned birds with attributes attached, but formally they were of class Animal. Now try, without changing the Animal class, to make the Bird :: get_flying_animal method, which yields the same birds using the same algorithm, but only now with the Bird class. Yes, the get_flying_animal method is actually much larger than in the example and you do not want to duplicate it.
Especially this situation can ruin your life if you cannot change the Animal class, or you don’t even know its source code, since they are written, for example, in sy. (As an exercise, try to write your own version of the HashLikeArray # to_a method, without using the Array :: [] or Array # replace methods. You simply have nowhere to duplicate the code, unless of course you are going to write a si library)
I figured out how to do it in a non-elegant way, by copying all the instance variables into a derived class object.
  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 


Now you can do this:
  def Bird.get_flying_animal Animal.get_flying_animal.type_cast(Bird) end 


If someone knows how to replace this clumsy crutch in the form of the type_cast method - write, this is a very interesting question.
Generally speaking, it is a controversial question how correct it is to redefine a class type for a subclass type. I would say that this is a dirty hack and not the PLO at all, but sometimes a very useful hack ... Ruby is such a special language, where every rule is needed only so that it is clear what can be broken. I think it would not go badly against the principles of ruby, if the object could have changed the class to any other after it had been created, by directing something like:
 x=X.new x.class = Y 

After all, a class actually means only an area for searching methods, constants and @@ - variables, so what difference does it make when to define a class: at the moment of creation or after. All responsibility for the integrity of the object from this moment lies on the shoulders of the user, but this is his right. Moreover, if the new class is a subclass of the old, then responsibility is mostly delegated to the old class.

Different ryushechki


Now on trifles:

Method returns self

In addition, I would suggest to introduce a method like
  def identity self end 

This is useful in order to write something like .collect (&: identity) instead of .collect {| x | x}
In the case of numerators and collectors, everything is solved simply - with the to_a method you can get a complete list of objects, but if you write your own method, the receiving block, then the function may be useful.
For example, let's make a method like the rail method Nil # try .
  class Object def try(method_name,&block) send method_name rescue yield end end 


Now we can do
 x.try(:transform,&:another_transform) 

And here we may need our identity method:
 x.try(:transform, &:identity) 

- if the conversion can be done, it will be done, no - the original object will be returned.

Object method # in? (Collection)

Rail method
 color.in?([:red,:green,:blue]) 
looks much better than
 [:red,:green,:blue].include?(color) 

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


For the sake of this method to connect the whole library (it seems ActiveSupport) is very lazy. I think that this method in the core would live well.

Negative form of interrogative methods

Would you like to write arr.not_empty? instead of awkward! arr.empty? There are a lot of similar interrogative methods in Ruby, but there are almost no negatives. Methods like not_empty? and not_nil? I would like to have built into the standard library. I prefer to add my own question methods to the negatives using method_missing (for obvious reasons method_missing is not recommended to be changed from the Object class)
  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 


We cut off file extensions

I have a particular hatred for the File module, in which there are no methods filename_wo_extname and basename_wo_extname. We fix:
  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" 


Some joy

I should make amends for such a mountain of complaints before — no matter what — the wonderful language of Ruby. Therefore, I will share one of the joyful news from the world of Ruby - Lazy numerators .

Source: https://habr.com/ru/post/141137/


All Articles