Скучая в эту дождливую праздничную погоду, наткнулся на занимательную статейку в блоге с говорящим названием Samurails, в которой описываются некоторые интересные ruby-трюки, которые наверняка будут интересны новичкам.

Итак, приступим.

Создаем хэш из массива


Проще простого. Ставим команду Hash перед любым массивом и получаем готовые пары ключ/значение:

Hash['key1', 'value1', 'key2', 'value2']

# => {"key1"=>"value1", "key2"=>"value2"}


Lambda как ->


Возможность проставлять лямбду при помощи -> появилась сравнительно недавно, будем пробовать:

a = -> { 1 + 1 }
a.call
# => 2

a = -> (v) { v + 1 }
a.call(2)
# => 3

Двойная звездочка (**)


Как вам такой метод:

def my_method(a, *b, **c)
  return a, b, c
end


а — это обычный аргумент. *b примет все аргументы после «a» и выведет их массивом, а вот **c принимает только параметры в формате ключ/значение, после чего отдаст нам хэш. Посмотрим примеры:

Один аргумент:

my_method(1)
# => [1, [], {}]


Набор аргументов:

my_method(1, 2, 3, 4)
# => [1, [2, 3, 4], {}]


Набор аргументов + пары ключ/значение

my_method(1, 2, 3, 4, a: 1, b: 2)
# => [1, [2, 3, 4], {:a=>1, :b=>2}]


По-моему, круто.

Обращаемся с переменной и с массивом одинаково


Иногда (лишь иногда) у вас может возникнуть желание запустить на объекте какой-либо метод без проверки его типа. То бишь обращаться с массивом так же как, скажем, с обычной переменной. В таких случаях можно пойти двумя путями — использовать [*something] или Array(something).

Давайте попробуем. Назначим две переменные: число и массив чисел

stuff = 1
stuff_arr = [1, 2, 3]


Используя [*] мы можем одинаково успешно итерировать по обеим переменным:

[*stuff].each { |s| s }
[*stuff_arr].each { |s| s }


Идентично:

Array(stuff).each { |s| s }
Array(stuff_arr).each { |s| s }



||=



Отличный ключ к сокращению количества строк нашего кода — использование ||=

Важно понять, что этот оператор работает так:

a || a = b # Верно


А не так:

a = a || b # Неверно!


Этот оператор прекрасно подходит для выполнения математических операций:

def total
  @total ||= (1..100000000).to_a.inject(:+)
end


Теперь мы можем использовать total в других методах, но посчитается он только один раз, что отразится на производительности нашего приложения.

Обязательные хэш-параметры



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

def my_method({})
end


теперь мы можем четко определить ключи, которые мы ждем на входе. Более того, мы можем определить их значения!

В данном примере a и b являются обязательными ключами:

def my_method(a:, b:, c: 'default')
  return a, b, c
end


Можем попробовать отправить в метод только «а» и нарваться на ошибку:

my_method(a: 1)
# => ArgumentError: missing keyword: b


Так как мы указали значение по умолчанию для «с», нам достаточно предоставить методу ключи «а» и «b»:

my_method(a: 1, b: 2)
# => [1, 2, "default"]


Или же можем отправить все три:

my_method(a: 1, b: 2, c: 3)
# => [1, 2, 3]


Можем быть более лаконичны:

hash = { a: 1, b: 2, c: 3 }
my_method(hash)
# => [1, 2, 3]


Генерируем алфавит или цепочку чисел при помощи range


Трюк достаточно старый, но вдруг кто-то не в курсе.

('a'..'z').to_a
# => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

(1..10).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


Tap


Tap — это отличный метод, способный улучшить читаемость нашего кода. Допустим, у нас есть класс:

class User
  attr_accessor :a, :b, :c
end


Теперь, допустим, нам захотелось создать нового пользователя, с атрибутами. Можно сделать это так:

def my_method
  o = User.new
  o.a = 1
  o.b = 2
  o.c = 3
  o
end


А можно использовать tap:

def my_method
  User.new.tap do |o|
    o.a = 1
    o.b = 2
    o.c = 3
  end
end

Комментарии (34)


  1. GrigoryPerepechko
    02.05.2015 22:11
    +4

    По-моему с Tap читаемость только ухудшилась


    1. mannaro
      02.05.2015 23:04

      Тут разумнее привести такой код:

      User.new.tap{|u| u.name = "Alex" }.save!
      

      Вместо
      u = User.new
      u.name = "Alex"
      u.save!
      
      Количество строк одна против 3. А также лишняя переменная.
      Но, как по мне, то читать легче без tap. Тут надо смотреть, чего мы хотим: мало строк или хорошая читаемость.


      1. shemerey
        03.05.2015 00:48

        Если для ActiveRecod то можно и без tap

        User.new do |user|
          user.name = 'Alex'
        end.save!
        
        


        тоже самое на create, update


        1. mannaro
          03.05.2015 00:51

          Ну, это для рельс.


          1. grossws
            03.05.2015 03:41
            +1

            Видел такой в куче библиотек относящих к рельсам чуть менее, чем никак.

            Паттерн обычно выглядит следующим образом:

            class Something
              def initialize(...)
                # initialization
                yield self if block_given?
              end
            end
            


          1. Renius
            03.05.2015 11:32

            точнее для ActiveRecord
            и для всех объектов принимающих блок


      1. GearHead
        03.05.2015 05:57

        то, что вы привели в качестве примера, не пройдёт ни один стайлгайд.
        .tap используется в основном для возвращения значения внутри метода:

        def make_admin
          User.new.tap do |user|
            user.role = 'admin'
          end
        end
        


        вместо

        def make_admin
          user = User.new
          user.role = 'admin'
          user
        end
        


        оно не сокращает количество строк, а улучшает читаемость


        1. kovyl
          03.05.2015 10:51

          Так а где улучшение читаемости-то? То же ж самое абсолютно.


          1. matiouchkine
            03.05.2015 12:20
            +1

            Да ни разу он не для улучшения читаемости просто. Бывает, что нужно что-то выполнить на промежуточном результате:

            def my_f
              User.new.tap { |u| puts “New user: #{u}” }
            end

            Напечатали лог и вернули вновь созданного юзера из функции. Вместо:

            def my_f
              user = User.new
              puts “New user: #{user}”
              user
            end


            1. kovyl
              03.05.2015 19:16

              Вот тут, согласен, tap вполне приятно смотрится.


              1. Dreyk
                06.05.2015 13:49

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


          1. GearHead
            03.05.2015 15:23

            нет лишней переменной.


            1. Alerticus
              03.05.2015 21:04

              зато есть лишний блок, лол


  1. grossws
    02.05.2015 23:20
    +7

    Возможность проставлять лямбду при помощи -> появилась сравнительно недавно
    В 1.9 она уже есть. Недавно?! xD


  1. GearHead
    03.05.2015 05:35

    В следующий раз лучше выбирайте пост для перевода. Если человек ведёт блог, это ещё не значит, что он профессионал языка.

    Кроме выше означенных проблем, вот ещё. Назовите разницу между
    a || a = b
    и
    a = a || b
    С логической точки зрения её нет.


    1. mukizu
      03.05.2015 08:11

      del


    1. modernstyle Автор
      03.05.2015 10:20
      +1

      Во втором случае нет смысла переназначать «a» если мы уже имеем ее в распоряжении.


      1. GearHead
        03.05.2015 15:24

        в ruby чистые присваивания практически ничего не стоят.


    1. Envek
      03.05.2015 15:30
      +1

      На самом деле a ||= b работает скорее как a = b unless a


  1. Hikedaya
    03.05.2015 10:00
    +3

    А хабраюзер total не обидится на то, что его будут в разных методах использовать? Да еще и считать, пусть только и один раз. «И тебя посчитали!» (с) :)


  1. mktums
    03.05.2015 11:46
    -1

    а вот **c принимет только параметры в формате ключ/значение, после чего отдаст нам хэш.

    О божечки, они изобрели **kwargs :D


    1. Envek
      03.05.2015 15:33

      И правильно сделали, этого не хватало, были всякие костыли типа extract_arguments в начале многих методов. Кстати говоря, именно поэтому 5-е рельсы активно перепиливают на использование **kwargs везде (следовательно минимальная версия Ruby там будет 2.2).


      1. grossws
        03.05.2015 16:57

        Оно же с 2.0 появилось? Например, здесь пишут, что это так: magazine.rubyist.net/?Ruby200SpecialEn-kwarg

        Это не считая того, что синтаксический сахар для последнего аргумента-хэша и в 1.9 был. Менее удобный, правда, так как значения аргументов по умолчанию выставлялись в теле функции и проверка наличия избыточных аргументов делалась вручную и довольно редко.


        1. Envek
          04.05.2015 14:49

          Как следует заработало только с 2.2 (а то и с 2.2.1).


  1. flamefork
    03.05.2015 13:39

    Важно понять, что этот оператор работает не так: a || a = b и не так: a = a || b:

    irb(main):001:0> b = :caveat
    => :caveat
    irb(main):002:0> a1 ||= b
    => :caveat
    irb(main):003:0> a2 || a2 = b
    NameError: undefined local variable or method `a2' for main:Object
    


    1. Tonkonozhenko
      03.05.2015 17:07

      Так все таки, как он работает?


      1. Loriowar
        03.05.2015 17:23

        Работает как и говорили выше

        a = b unless a
        

        Поэтому много проблем влечёт ||=, так как забывают люди постоянно про то, что с Bool данную конструкцию использовать нельзя.

        irb(main):001:0> a = 42
        => 42
        irb(main):002:0> a ||= 77
        => 42
        irb(main):003:0> a = false
        => false
        irb(main):004:0> a ||= true
        => true
        irb(main):005:0> a
        => true
        

        Вроде как и понятно почему, но когда идёт несколько идентичных методов или присваиваний и среди них забываешь про Bool становится очень обидно за убитое на дебаг время.


        1. Tonkonozhenko
          04.05.2015 03:24

          За bool спасибо, не задумывался об этом

          Но вот

          a||=b
          

          не совсем
          a = b unless a
          

          потому что
          2.2.0 :001 > a = :symb
           => :symb 
          2.2.0 :002 > b ||= a
           => :symb 
          2.2.0 :003 > b = a unless b
           => nil 
          2.2.0 :004 > b
           => :symb 
          

          а скорее
          if defined?(b) && b then b else b = a end
          


      1. flamefork
        04.05.2015 11:22

        Собсно я еще над этим подумал, и получается, что как раз

        a = a || b
        

        так оно и работает :) То есть так, как в посте помечено коментарием «Неверно». Поправьте, в чем неправ.


  1. Helsus
    03.05.2015 15:33

    def total
      @total ||= (1..100000000).to_a.inject(:+)
    end
    

    Только когда пишите такое, помните, что это не thread-safe.


    1. hats
      03.05.2015 21:48

      как бы вы переписали этот метод, чтобы он стал 'thread-safe'?


      1. Helsus
        03.05.2015 23:55

        Синхронизировать (см.), или, если возможно, перенести в конструктор без ||


  1. Drakula2k
    04.05.2015 16:16

    Стоит отметить, что ||= нужно применять с осторожностью для вычислений, которые могут вернуть nil, в таком случае результат не будет закеширован. Для решения этой проблемы есть гемы типа memoist.


    1. achempion
      04.05.2015 22:52

      код почему-то не подсвечивается

      def products

      return @cached[:result] if @cached.is_a?(Hash)

      #…
      # нужные операции
      #…

      @cached = {result: result}

      end