Войти на сайт

Авторизация, ждите ...
×

ТЕМА: Операторы в руби

Операторы в руби 9 года 8 мес. назад #73056

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПроект года 1 местоПроект месяца 1 местоПрограммист RubyУчитель
Сегодня я вам расскажу о том, чего на самом деле нет - о операторах в руби. Вы можете спросить: а как же +, -, * и т.п. Так вот, их нет. И ложки тоже нет кстати.
Есть только матрица объекты. Методы - тоже объекты, а все эти операторы - на самом деле вызов методов.
1 + 1 #=> 2
#То же самое, что и
1.+(1) #=> 2

1 * 2 #=> 2
#То же самое, что и 
1.*(2) #=> 2
#и так далее
Но не все так просто, как на первый взгляд
1 + 2 * 3 + 4 
#это выражение нельзя представить в таком виде
1.+(2).*(3).+(4)
#правильно будет так
1.+(2.*(3)).+(4)
Поэтому в руби есть приоритет операторов, некоторые выполняются раньше, некоторые позже. Но я специально не буду вам о них рассказывать - математические операторы исполняются точно так, как мы привыкли в школе, - деление и умножение раньше чем сложение и т.п. Остальные приоритеты - совсем не очевидны, поэтому лучше их группировать при помощи скобок
expression_one && expression_two || expression_three && expression_for 
#не стоит пологаться, что кто то помнит что сильнее - && или ||. Используйте скобки!

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

Но что нам это дает? Возможность останавливать пули определить \ переопределить операторы и получить отличный код.
Допустим нам нужно написать класс урона, ну вы знаете - "Алистер нанес скелету 20 единиц святого урона"
class Damage
end
Уже хорошо, теперь нужно добавить атрибуты - для простоты оставим единицы и природу.
class Damage
  attr_reader :points, :nature
 
  def initialize(points, nature)
    @points, @nature = points, nature
  end
end
Почему я использовал attr_reader а не attr_accessor - я долго мучался, как же мне стоит писать, но в итоге решил - если предполагается установка атрибута из вне - использую attr_accessor, если нет - attr_reader и работаю с инстанс переменными (если мне нужно атрибут установить).
Теперь мы можем написать:
alister_damage = Damage.new 20, 'holly'
morrigan_damage = Damage.new 40, 'fire'
А сколько нанесли Алистер и Мориган вместе? Нужно добавить сложение!
#Дальше буду писать как будто я в теле класса.
def +(other)
  Damage.new points + other.points, [nature, other.nature].flatten
end
Сложение не должно изменять текущий объект, поэтому создаем новый. Points в новом объекте будут равны сумме points в исходных объектах, nature - тут уже на выбор разработчика. На мой взгляд, если, у урона природа одна, то и nature для такого объекта должен возвращать собственно строку природы. Если же несколько - то от массива не уйдешь. Обратите внимание, как я использовал flatten, это мне позволяет не думать, сколько там природ у слагаемого. Метод в таком виде - пример утиной типизации - если в двух словах то нам не важно, какой объект передается методу, мы предполагаем что этот объект скорее всего принадлежит классу Damage "Если кто то крякает как утка и выглядит как утка - значит это утка" (c) - отсюда название такого подхода. Плюс тут в том, что кода меньше, ну а минус - иногда получаются такие веселые баги, что век не поймаешь. Поэтому я почти всегда стараюсь хоть как то валидировать то, что ко мне пришло в метод.
def +(other)
  check other, Damage
  Damage.new points + other.points, [nature, other.nature].flatten
end
 
private
 
def check(value, klass)
  unless value.is_a? klass
    raise ArgumentError "expect #{value} to be a #{klass.name}"
  end
end
Это пример защитного программирования. Идея в том, что лучше сразу сообщить о ошибке, ведь чем раньше вы обнаружите ошибку - тем дешевле ее исправить.
party_damage = alister_damage + morrigan_damage
party_damage.points #=> 60
party_damage.nature #=> ['holly', 'fire']

Хорошо, а что если на цели дебафф, увеличивающий урон вдвое? А если бафф, уменьшающий урон?
def *(number)
  check number, Numeric
  Damage.new points * number, nature
end
 
def /(number)
  check number, Numeric
  Damage.new points / number, nature
end

Теперь мы можем сделать так:
party_damage = party_damage * 2
# или в сокращенной форме
party_damage *= 2

Ну так кто круче, Алистер или Морриган? Кто нанес больше урона? Нам нужны методы сравнения >, <, <=, == и т.д. Для начала нужно решить, будет ли влиять природа на сравнение урона? Вспоминая свои годы жизни в WoW, я думаю что нет. Будем сравнивать только points. Мы можем сами определить каждый из этих методов сами, а можем пойти легким путем. Comparable - это миксин из стандартной библиотеки, включив его мы получаем сразу все методы сравнения в классе. Но мы должно определить последний на сегодня оператор <=> (Лодочка). Этот метод должен возвращать -1 когда объект меньше, чем предоставленный, 0 когда объекты равны, 1, когда больше. (Мы же пойдем совсем легким путем и просто вызовем <=> у атрибута points, ведь это число).
include Comparable
def <=>(other)
  check other, Damage
  points <=> other.points
end

В итоге у нас получился полный класс:
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]


И вот как мы его используем:
alister_damage = Damage.new 20, 'holly'
morrigan_damage = Damage.new 50, 'fire'
 
alister.kindness #=> 2 Алистер жутко добрый
alister_damage *= alister.kindness
alister_damage.points #=> 40

morrigan.malice #=> 2 та еще злюка эта Мориган
morrigan_damage /= morrigan.malice
morrigan_damage.points #=> 25

morrigan_damage >= alister_damage #=> false
alister_damage > morrigan_damage  #=> true
alister_damage == morrigan_damage #=> false

party_damage = morrigan_damage + alister_damage
party_damage.points #=> 65
party_damage.nature #=> ['holly', 'fire']
В общем всем теперь ясно, что Алистер круче, а Морриган желаю быть добрее.
Так же, определив методы равенства в нашем классе, мы получаем доступ к другим фишкам, которые на этих методах основываются. На ум приходят только массивы, но уверен что есть еще.
arr = [Damage.new(100, 'acid'), Damage.new(50, 'cold'), Damage.new(120, 'fire')]
arr.max.nature #=> 'fire'
arr.min.points #=> 50
arr.sort.map { |dmg| "#{dmg.nature} - #{dmg.points}" } #=> ['cold - 50', 'acid - 100', 'fire - 120']

И для тех кто дочитал до сюда - бонус! Два самых мутных оператора в руби [] и []=, ведь аргументы для этих методов передаются по особому:
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]


Это все, что я хотел сказать, спасибо за внимание!
Последнее редактирование: 9 года 8 мес. назад от Iren_Rin.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Cerberus, strelokhalfer, Lipton, Amphilohiy, Jas6666, yuryol, MaltonTheWarrior

Операторы в руби 9 года 8 мес. назад #73062

  • Amphilohiy
  • Amphilohiy аватар
  • Вне сайта
  • Светлый дракон
  • Сообщений: 547
  • Спасибо получено: 666
  • Победитель Сбитой кодировки2 место ГотвОраторПрограммист RubyУчитель
Сначала говоришь, что операторов нет... А потом утверждаешь, что у оператор есть приоритет...
В целом согласен, что у Руби такой синтаксис, что в итоге перегрузка операторов выглядит как просто объявление метода в принципе, но отличия все же есть.
Как ты заметил - приоритет, обычные методы так не делают... вроде бы...
Нельзя определить метод plus и потом швыряться им как
foo plus bar
Да, ты сказал, что синтаксический сахар, дело обычное, но все же есть набор методов, для которых позволен этот сахар. И есть они ни что иное как операторы.
Но чую, что ты имел ввиду, что они просто являются методами, и я тут ни за что на тебя напал :)
Пример, конечно, показательный, но немного пугает. Можно сложить гигантский урон не очень хорошего элемента, а затем впихнуть единичку, на которую у противника слабость... Но лучше чем обычные комплексные числа :)
А чтобы в итоге не выглядело, что я просто жалуюсь, то я дам ссыль на сайт с приоритетами, для ленящихся гуглить :laugh: Приоритеты.
А вообще хотел бы придраться немного к контенту. Использование оператора <=> в методе max указано вскользь в коде. Можно было бы явнее описать использования таких операторов по умолчанию (если не задана другая функция) например при сортировке массива. Ну и обратить внимание на то, что можно подобные функции писать самому, опираясь на эти операторы, и они все равно будут довольно гибкими. /cheers
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Iren_Rin

Операторы в руби 9 года 8 мес. назад #73064

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПроект года 1 местоПроект месяца 1 местоПрограммист RubyУчитель
1) Ну хотя бы с тем что Алистер круче Морриган ты согласен, это радует.
2) Я приследовал две цели - показать что операторы в руби это методы и то как их можно переопределить и использовать. Не вина метода + что интерпретатор может распознавать его вызов без точки :). Но это действительно самые настоящие методы, и это не перезагрузка операторов так выглядит, а самое настоящее определение самого настоящего метода. Ты можешь его получить при помощи method, увидеть в public_methods, вызвать при помощи __send__ и public_send, эти методы наследуются, подмешиваются и т.п. и т.д.
1.public_send :+, 1 #=> 2
Я допишу это в начальный пост.
3) Перечитал примеры и понял что их недостаточно в части про <=>, добавлю в начальный пост (я просто хотел показать как подключив один миксин и определив один метод вы можете получить целый вкусный кусок функциональности за бесплатно).
4) Как ты мог заметить, мультипликаторы для урона Алистера и урона Морриган я применил до того как сложил их. Естественно, в такой реализации все мультипликаторы должны быть применены до складывания урона.

Спасибо за конструктивную критику!
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Amphilohiy

Операторы в руби 9 года 8 мес. назад #73066

  • Amphilohiy
  • Amphilohiy аватар
  • Вне сайта
  • Светлый дракон
  • Сообщений: 547
  • Спасибо получено: 666
  • Победитель Сбитой кодировки2 место ГотвОраторПрограммист RubyУчитель
Ну, сложение уронов штука сложная, т.к. должна происходить в контексте сопротивлений цели. Тут "операторы" не очень подходят (разве что сделать операторы с разными классами: существо - урон, или существо + урон как лечение. Смущает только то, что придется дублировать существо с иным значением хп, ибо менять значение операнда плохой, очень плохой тон), но не стоит из-за этого менять пример.
Я в основном к тому, что в тех же сях перегрузка оператора выглядит как определение функции, правда можно ли их потом вызвать через точку - не проверял. Но самое главное - я думаю что мы от самого термина "оператор" не откажемся, ибо имеет много синтаксических сходств с другими ЯП, да и Руби к ним относится по особому. Но посыл понял - сформировать более верное представление (что хоть операторы и особенные с виду, в итоге они просто методы с подходящим именем)
И, извини слоупока, есть же тема операторов [](key) и []=(key, value). Новичкам может быть непрозрачно, особенно с учетом продвинутого сахорка (значение ключа между именем функции). Но не факт, что потребуется, возможно они у нас смышленые :)
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Последнее редактирование: 9 года 8 мес. назад от Amphilohiy.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Iren_Rin

Операторы в руби 9 года 8 мес. назад #73067

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПроект года 1 местоПроект месяца 1 местоПрограммист RubyУчитель
Добавил описание [] и []= как бонус в самом конце, они просто немного мутноваты :)
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Amphilohiy
Время создания страницы: 0.719 секунд