Чего мне хотелось бы от будущих версий Ruby, и как я справляюсь сейчас

Добрый день, Хабр.
Я около года работаю с Ruby и хотел бы написать о некоторых вещах, которых лично мне там часто не хватает, и которые я хотел бы видеть встроенными в язык. Пожалуй лишь пара из этих пунктов являются действительно серьезными недоработками, с остальными можно легко справиться подручными средствами.
Вроде бы недоработки и мелочь, но они ощутимо осложняют работу — приходится писать собственные библиотеки вспомогательных методов, которые и в гем не выделишь — уж больно маленькие, и без них некомфортно. А порой открываешь чужой код — и видишь там точно такие же вспомогательные функции как у тебя. Это как мне думается знак, что стандартная библиотека языка недоработана. Что же, будем надеяться, кто-нибудь из разработчиков прочтет текст и закоммитит патч. ;-)
Итак, начнем по порядку:
  • Перегрузка метода разными списками аргументов, как в C++
  • Отобразить хэш и получить из него другой хэш, а не массив
  • Преобразовать экземпляр класса в экземпляр его же собственного подкласса
  • Разные рюшечки

Перегрузка метода разными списками аргументов, как в C++


Вот это на мой взгляд одна из самых больших проблем руби. Он не позволяет определить метод по разному для разных типов аргументов. В результате приходится писать сложные конструкции разбора списка параметров, проверять типы, наличие-отсутствие аргументов итд. Я не претендую на решение этой проблемы, чтобы её решить надо перелопачивать исходники, писать много кода и ещё больше тестов. Поэтому я покажу лишь реализацию, которая в простейших случаях может сэкономить вам пару строк кода.
Для этого мы воспользуемся alias-method-chain'ингом.
	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
	  x.f       # => "original version []"
	  x.f 3, 'a'  # => "original version [3,\"a\"]"
	  x.f 3, 4  # => "pair numeric version 3 & 4"
	  x.f 3     # => "numeric version 3"
	  x.f '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
	


Вместо этого, конечно, можно просто написать одну функцию
	    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), но думаю их можно решить слегка расширив метод overload. Если сообществу покажется, что такой подход неплохо было бы развить — я попробую сделать на эту тему отдельную статью и расширю код.
Мне хотелось бы в будущих версиях руби иметь встроенные средства для подобной перегрузки метода.

Отобразить хэш и получить из него другой хэш, а не массив


Честно сказать я в недоумении, почему этого метода нет прямо в Enumerable, это одна из самых частых конструкций, которая бывает нужна. Её я безусловно хотел бы иметь отдельным методом, и даже не во втором руби, а чем раньше тем лучше (тем более, что это плевое дело).
У меня часто возикает задача пройтись по хэшу и сделать из него другой хэш. Этакий map. Вот только map для хэша вам выдаст что? Правильно, массив. Обычно приходится выкручиваться подобной конструкцией:
{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}

Есть ещё один вариант
Hash[{a: 1, b:2, c:3, z:26}.map{|k,v| [k,v.even?]}]
Этот вариант чуть попроще и погибче т.к. позволяет отображать не только значения, но и ключи. Однако совершенно явно, что массивам не хватает метода to_hash, чтобы писать не Hash[arr], а arr.to_hash.
Однако не ко всем массивам такой метод будет применим, вероятно по этим причинам метода Array#to_hash и нет в ядре(см. обсуждение).
Это намекает, что стоит сделать производный от Array класс HashLikeArray, принимающий лишь массивы вида [[k1,v1],[k2,v2],...], но об этом в следующем пункте.

А пока реализуем простой hash_map, основанный на методе inject:
	  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. Вы, конечно, можете попробовать написать свою реализацию этого метода, но на самом-то деле вам нужна лишь обертка, которая превращает результат исходного Hash#to_a из класса Array в подкласс HashLikeArray.
Давайте попробуем написать (не заморачиваясь сейчас никакими 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
	


Справились. Теперь задачка посложнее, пусть у нас есть класс менее проработанный, чем 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
	

Теперь пусть у вас есть старый метод get_flying_animal, который писался, когда класса Bird ещё не существовало. Animal::get_flying_animal всегда возвращал птиц с проставленными аттрибутами, но чисто формально они были класса Animal. Теперь попробуйте не меняя класса Animal сделать метод Bird::get_flying_animal, который выдает тех же самых птиц по тому же алгоритму, но только теперь класса Bird. Да, метод get_flying_animal на самом деле гораздо объемней, чем в примере и дублировать его вы не хотите.
Особенно эта ситуация может испортить вам жизнь, если вы не можете менять класс Animal, либо даже не знаете его исходников, т.к. они написаны, например, на си. (В качестве упражнения, попробуйте написать свою версию метода HashLikeArray#to_a, не используя методов Array::[] или Array#replace. Дублировать код у вас просто неоткуда, если конечно, вы не собираетесь писать си-шную библиотеку)
Я придумал как это сделать лишь неэлегантным способом, копируя все переменные экземпляра в объект производного класса
	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 — пишите, это крайне интересный вопрос.
Вообще говоря это спорный вопрос, насколько корректно так переопределять тип класса на тип подкласса. Я бы сказал, что это грязный хак и вообще не ООП, но иногда очень полезный хак… Руби — это такой специальный язык, где каждое правило нужно лишь за тем, чтобы было ясно, что именно можно нарушить. Думаю это не пошло бы сильно вразрез с принципами руби, если бы у объекта можно было поменять класс на любой другой уже после создания, директивно сказав что-то вроде:
x=X.new
x.class = Y

Ведь класс означает фактически лишь область для поиска методов, констант и @@-переменных, так какая разница, когда определять класс: в момент создания или после. Вся ответственность за целостность объекта с этого момента лежит на плечах пользователя, но это его право. Более того, если новый класс — подкласс старого, то ответственность большей частью делегирована старому классу.

Разные рюшечки


Теперь по мелочам:

Метод возвращающий self

Кроме того я бы предложил ввести метод вроде
	def identity
	  self
	end
	

Полезно это для того, чтобы можно было в блоках писать что-то вроде .collect(&:identity) вместо .collect{|x| x}
В случае с нумераторами и collect-ом все решается просто — методом to_a можно получить полный список объектов, а вот если вы пишете свой метод, принимающий блок, то функция может оказаться полезной.
Например сделаем метод наподобие рельсовского метода Nil#try .
	class Object
	  def try(method_name,&block)
	    send method_name
	  rescue
	    yield
	  end
	end
	


Теперь мы можем сделать
x.try(:transform,&:another_transform)

И вот тут нам может понадобиться наш метод identity:
x.try(:transform, &:identity)

— если преобразование сделать можно, оно будет сделано, нет — будет возвращен исходный объект.

Метод Object#in?(collection)

Рельсовский метод
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? вместо неуклюжего !arr.empty? Подобных вопросительных методов в руби очень много, а отрицаний почти что нет. Методы типа not_empty? и not_nil? хотелось бы иметь встроенными в стандартную библиотеку. Собственные вопрос-методы я предпочитаю доопределять отрицаниями с помощью method_missing (по понятным причинам method_missing у класса Object менять не рекомендуется)
	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. Поэтому поделюсь одним из радостных известий из мира руби — Ленивые нумераторы .

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 22

    +7
    > Перегрузка метода
    Имхо тут проблема не в самом руби, а том, что вы хотите программировать на нём, как на си/яве/шарпе.
    Уврен, этого никогда не сделают, и не только потому, что в руби2 обещают ввести именованные аргументы фукцииям, но и потому, что ваш пример использования перегрузки сам по себе является примером дурного кода.

    if arg.is_a? String
    'string version ' + arg.to_s
    elsif args.is_a? Array
    'array version ' + arg.to_s
    elsif args.is_a? Numeric
    'numeric version ' + arg.to_s
    end

    Вернее конечно в совсем тривиальных случаях, с логикой в одну-две строчки, подобный подход имет право на жизнь, но во всех остальных ситуациях следует доработать иерархию классов и использовать полиморфизм. (кстати, не рекомендуют использовать is_a?, а вместо применять kind_of?)

    Ещё одна причина в том, что логика работы самого руби проста как доска, и для любого идентификатора действуют лишь два правила:
    1. Это локальная переменная
    2. Если это не локальная переменная, то это вызов фунции из self.
    Ну а при вызове ф-ии, ф-ия ищется в синглетон классе, в текущем классе, а затем по всем ancestors, после чего идёт вызов method_missing.
    И усложнять эту логику ради ситуаций, нарушающих принцип «не спрашивай меня, кто я, а говори мне, что делать»… зачем?

    > Отобразить хэш и получить из него другой хэш, а не массив
    Вот тут соглашусь, inject не всегда удобно использовать.

    > Преобразовать экземпляр класса в экземпляр его же собственного подкласса
    Мне кажется, всё дело опять в коде, нарушающем принцип «не спрашивай меня, кто я, а говори мне, что делать».

    Ведь и сейчас рабочих вариантов для подобной ситуации достаточно.
    Можно не наследовать Bird от Animal, а делегировать.
    Можно нужный функционал Bird вынести в модуль и полученное от Animal.get_flying_animal расширить этим модулем.
    Можно манкипатчнуть сам Animal, чтобы он возвращал сразу нужное.
    Или писать код так, чтобы ему всё равно было, что там Animal или Bird или что-то другое.

    Вариант с заменой x.class при попытке реализации в язык, весьма вероятно, вызовет очень много побочных эффектов, которые сходу даже предположить сложно.

    > Метод Object#in?(collection)
    В активсуппорте много всяких вкусностей, и всем нравятся разные. Тогда уж сразу весь гем в стандартную библиотеку, чтобы никому не было обидно.
    Хотя опять зачем? Можно просто подключить этот гем в ваш проект :)

    > Отрицательная форма вопросительных методов
    if !arr.empty? -> if arr.any? -> unless arr.empty?
    if !a.nil? -> if a -> unless a.nil?
    Плюс .blank? ещё в активсуппорте

    > Отрезаем расширения файлов
    По-моему это тоже есть, толи какой-то класс, толи метод в одном классов. Не помню, кажется где-то видел и даже использовал.
      0
      Ага, насчет расширений etc.: ruby-doc.org/core-1.9.3/File.html#method-c-extname
        0
        Да, этот-то метод есть (собственно в коде я его и использовал). А метода, который это расширение не оставляет, а убирает — нет.
          0
          File.basename(path, File.extname(path))
        0
        Я понимаю, что мой пример из первого пункта — скорее для полиформизма. А вот рельсовские методы, например, которые принимают то класс, то блок, то имя функции? Что в таких случаях рекомендуете, методы типа to_proc, все сводящие в итоге к одной ситуации, или ещё как-то?

        Думаю, что я действительно не смог до конца свыкнуться с duck-typing'ом. Всё-таки хочется разграничивать различные сущности Bird и Animal, даже если это не обязательно для работы кода. Я напомню, что начиналось всё с предложения сделать класс сходный с классом Array, только более узкого назначения. Я предлагал упражнение — попытаться это сделать без методов Array#replace, Array::[]. Какое предложите решение?

        Да, вероятно, гем придется подключать. Вот только если он будет почти всем нужен — зачем его держать отдельно от стандартной библиотеки (тем более что там есть куда более редко используещиеся модули)?

        (![nil].empty?) != ([nil].any?)
        unless — неплохой вариант, но часто нужна форма типа
        if arr.not_empty? && arr.shift.my_condition?
        Здесь unless неуместен, а разбивать условие на два условия — вообще тупо.

        Библиотечным методом можно только получить расширение файла, а отрезать его только вручную.
          0
          if arr.present?… из того же ActiveSupport
        0
        Касательно второго пункта, насколько я понимаю, вот такая конструкция

        hash.update(hash) {|key,v1| f(v1)}

        делает то же самое штатными средствами.
          0
          Да, спасибо! Не знал о том, что update может использоваться с блоком. Единственное, стоит отметить, что это деструктивный метод. А можно ли как-то похожим образом сделать новый хэш, не испортив старый?
            0
            hash.dup.update(hash) {|key,v1| f(v1)}

            В смысле, вот так?
              0
              Ммм, туплю. Убедили.
          0
          Кстати, неявный вызов блока быстрее явного где-то в 2 раза. Плюс правило хорошего тона — клонировать текущий объект. Поэтому hash_map лучше написать так:

          class Hash
          def hash_map(&block)
          dup.inject({}) do |hsh,(k,v)|
          hsh.merge( k => yield(v))
          end
          end
          end
            0
            Про клонирование — ошибся, а про неявный блок — в силе :)
              0
              Спасибо! Буду знать.
              Про dup — это скорее касается случая, когда вы возвращаете наружу объект, описывающий внутреннее состояние.
                0
                В коде ошибка, следует читать def hash_map, без всяких аргументов. Про dup отписался выше.
              0
              Мне кажется что авто не смог отвыкнуть от других языков за этот год… Ради интереса — можете ли вы показать реализацию quicksort в виде 2-3 строк и в виде эффективного алгоритма?
                0
                Вполне возможно, что и не смог.
                def quicksort(arr)
                return arr if arr.size <= 1
                x = arr.sample
                quicksort(arr.select{|el|el < x}) + [x]*arr.count(x) + quicksort(arr.select{|el|el > x})
                end


                Что вы имеете ввиду под эффективным алгоритмом? Проводить операции in-place, не использовать лишний count и два select-а переделать в один group_by, например?
                  0
                  именно in place, не используя лишней памяти и pivot выбирать как нужно.
                    0
                    в общем суть сделать O(n*log(n)) в среднем, а не O(n^2)
                      0
                      Мне кажется обе наших реализации будут такими.
                        0
                        не будут. выбор первого как пивот это очень плохо. особенно на массивах которые уже отсортированы
                          0
                          Ну, у вас достаточно убрать pop и поставить delete_at(rand(0..size)) — делов-то. Хотя, конечно, руби при этом будет лишний раз пересобирать массив скорее всего.
                    +1
                    def quicksort(arr)
                      (pivot = arr.pop) ? quicksort(arr.select{ |i| i <= pivot }) + [pivot] + quicksort(arr.select{ |i| i > pivot }) : []
                    end
                    
                    


                    тоже вариант, хоть и не самый лучший

                Only users with full accounts can post comments. Log in, please.