Часто мы видим такую ситуацию. Разработчик, имеющей много опыта с одним языком программирования, пытается немного «поиграться» с другим и поспешно пишет их «сравнение». Обычно такое сравнение малополезно, но завлекательные заголовки приносят им много трафика.

Полагаю, вам будет интересно более честное сравнение от разработчика, который любит PHP, Ruby и имеет много лет опыта работы с обоими этими языками. Я пишу эту статью не с целью определить, какой из языков «круче», а чтобы рассказать о нескольких ключевых вещах, которые мне нравятся в Ruby и экосистеме этого языка.


Концептуальные различия


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

Так что перед тем, как перейти к перечислению «что мне нравится в Ruby по сравнению с PHP», я считаю важным рассказать об этих концептуальных различиях.

Метод, переменная, свойство?


PHP предлагает разный синтаксис для доступа к свойствам объектов, методам и переменным. Ruby – нет.

PHP
$this->turtle   # свойство экземпляра
$this->bike()   # метод
$apple          # переменная

Ruby
@turtle         # свойство экземпляра
turtle          # "свойство экземпляра" с помощью attr_reader: :turtle
bike            # метод
apple           # переменная

Педанты напомнят мне, что конструкция «attr_reader :turtle» просто создаст динамически метод, который можно использовать как геттер для "@turtle", что делает доступ к «turtle» и «bike» одинаковым. Тем не менее, PHP разработчик, который видит код использования «turtle» без явного указания метода или переменной с таким именем, будет сильно удивлен.

Такой синтаксис языка не то чтобы доставляет много проблем, но неприятные моменты всплывают регулярно. Иногда разработчики в вашей команде меняют что-то с «attr_reader» на полноценный метод (или наоборот), что приводит к диковато выглядящему для новичков коду.

С другой стороны, этот синтаксис позволяет гибко менять API и быстро накладывать заплатки в критический момент. Удалили поле, но есть много ссылающегося на него кода и использующий его JSON?

class Trip
  def canceled_at
    nil
  end
end

Вот такой маленький хак. Все обращения к “trip.canceled” получат “nil”, что вполне нас устраивает. Убрать такие вызовы можно потом, когда будет время.

Type Hinsts против Утиной Типизации


В мире PHP, «type hints» одновременно и странная, и замечательная штука. В таких языках как Go, вы обязаны указывать типы для аргументов функций и возвращаемого значения. В версии 5.0 PHP была добавлена поддержка опциональных «type hints» для аргументов. Вы можете указывать массивы, имена классов, интерфейсов, и, с недавнего времени, блоков кода.

С версии 7.0 в PHP наконец-то добавили поддержку «type hints» для возвращаемых значений и таких типов как «int», «string», «float» и тд. Сделано это с помощью RFC «Scalar Type Hints», которую приняли как полностью опциональную после бурных и долгих обсуждений.

В Ruby ничего этого нет.

Примечание переводчика. Тут автор несколько лукавит. Несмотря на то, что в синтаксисе самого языка этого нет, язык – это еще не все. Есть еще экосистема. А в экосистеме есть популярная и хорошо себя зарекомендовавшая библиотека «contracts.ruby», которая делает все то же самое, но в рантайме.

Вместо того, чтобы говорить «аргумент должен быть экземпляром класса, имплементирующего implements» и помнить, что в этом интерфейсе должен быть метод «bar(int $a, array $b)», вы говорите «аргумент может быть чем угодно до тех пор, пока он откликается на метод ‘bar’. А если нет, то мы что-нибудь придумаем».

Ruby

def read_data(source)
  return source.read if source.respond_to?(:read)
  return File.read(source.to_str) if source.respond_to?(:to_str)
  raise ArgumentError
end

filename = "foo.txt"
read_data(filename) #=> считываем содержимое foo.txt с помощью 
                    #   File.read()

input = File.open("foo.txt")
read_data(input) #=> считываем содержимое foo.txt с помощью
                 #   переданного объекта для работы с файлами 

Пример действительно гибкого кода, но для некоторых это еще и «дурно пахнущий код». Особенно для языков вроде PHP, где «int(0)» или «int(1)» считаются вполне себе булевыми типами. По крайней мере в weak mode. Принимать на вход что угодно и просто надеяться, что у этого «чего угодно» есть нужные методы, выглядит чуток страшновато.

В мире PHP мы обычно используем для таких целей два разных метода или функции:

function read_from_filename(string $filename)
{
    $file = new SplFileObject($filename, "r");
    return read_from_object($file);
}

function read_from_object(SplFileObject $file)
{
  return $file->fread($file->getSize());
}

$filename = "foo.txt";
read_from_filename($filename);

$file = new SplFileObject($filename, "r");
read_from_object($file);

Если мы захотим использовать в PHP утиную типизацию, мы легко можем это сделать:

function read_data($source)
{
    if (method_exists($source, 'read')) {
        return $source->read();
    } elseif (is_string($source)) {
        $file = new SplFileObject($source, "r"));
        return $file->fread($file->getSize());
    }
    throw new InvalidArgumentException;
}

$filename = "foo.txt";
read_data($filename); #=> считываем содержимое foo.txt с помощью 
                      #   SplFileObject->read();

$input = new SplFileObject("foo.txt", "r");
read_data($input); #=> считываем содержимое foo.txt с помощью
                   #   переданного объекта для работы с файлами

Как видите, в PHP можно использовать любой способ, при этом язык не пытается навязать вам какой-то подход.
Очень популярно делать вид, что «Ruby лучше PHP» потому что в Ruby есть утиная типизация. Вам может нравится утиная типизация, но в PHP можно использовать как ее, так и type hints. А вот в Ruby type hints использовать нельзя. Так что с моей точки зрения в этом вопросе выигрывает как раз PHP, который позволяет писать код любым удобным разработчику способом.

Нужно заметить, что многим PHP программистам type hints не нравится и они очень расстроились, когда в 7-й версии PHP этот механизм был еще больше расширен.

Любопытно, что в Python этого механизма никогда не было, так же как в Ruby. А вот в последней версии его добавили. Как на это отреагируют разработчики?

Прикольные фичи


Приняв различие в типизации между PHP и Ruby, я смог фокусироваться на интересном. На тех фичах, которые я регулярно использовал в моей работе и которые приносили мне явную пользу.

Вложенные классы


Концепция вложенных классов выглядит немного странно для PHP разработчика. Наши классы вложены в области видимости. И класс, и область видимости могут иметь одно и то же имя. Так что когда нам нужно создать вспомогательный класс, мы просто используем область видимости и все.

Представим себе, что у нас есть класс «Box», который может взрываться исключением «ExplodingBoxException»:

namespace Acme\Foo;

class Box
{
    public function somethingBad()
    {
      throw new Box\ExplodingBoxException;
    }
}

Определение класса для исключения должно где-то находиться. Мы можем разместить его перед классом «Box», но тогда у нас будет два класса в одном файле, что не есть хорошо. И, к тому же, нарушит PSR-1:

This means each class is in a file by itself, and is in a namespace of at least one level: a top-level vendor name.

Так что определение этого класса помещается в отдельный файл:

namespace Acme\Foo\Box;

class ExplodingBoxException {}

Чтобы загрузить этот класс для исключения, нам придется воспользоваться autoloader и напрячь файловую систему. И это далеко не бесплатная операция. Начиная с PHP версии 5.6 нагрузка на подобные операции снижается, если включено кеширование опкодов, но это все равно лишняя работа при выполнении вашего кода.

В Ruby вы можете разместить определение класса внутри другого класса:

module Acme
  module Foo
    class Box
      class ExplodingBoxError < StandardError; end

      def something_bad!
        raise ExplodingBoxError
      end
    end
  end
end

Вложенный класс можно использовать как внутри «родительского», так и вне его:

begin
  box = Acme::Foo::Box.new
  box.something_bad!
rescue Acme::Foo::Box::ExplodingBoxError
  # ...
end

Выглядит странновато, но работает великолепно. Класс используется только в другом классе? Группируем!

Другим примером будет миграция баз данных.

Миграции встроены во многие популярные PHP фреймворки, от CodeIgnater до Laravel. Все, кто их использовал, знают одну неприятную особенность. Если вы упоминаете в миграции модель или класс, а впоследствии его меняете, то старые миграции будут ломаться самым непредсказуемым и фееричным образом.

Ruby изящно решает эту проблему с помощью вложенных классов:

class PopulateEmployerWithUserAccountName < ActiveRecord::Migration
  class User < ActiveRecord::Base
    belongs_to :account
  end

  class Account < ActiveRecord::Base
    has_many :users
  end

  def up
    Account.find_each do |account|
      account.users.update_all(employer: account.name)
    end
  end

  def down
    # Update all users whose have account id to previous state
    # no employer set
    User.where.not(account_id: nil).update_all(employer: nil)
  end
end

Вместо глобальных ORM моделей «User» и «Account» используются вложенные версии, которые отражают положение дел на момент создания миграции. Это гораздо лучше, чем ссылаться на объекты, которые могут поменяться в будущем.

Еще раз повторю: для некоторых разработчиков такой подход выглядит дико. Пока вы пару раз не столкнулись с описанными выше проблемами.

В случае с миграциями PHP разработчики вынуждены создавать сложный код или писать SQL код вручную – что является пустой тратой времени по сравнению с подходом Ruby, при котором язык позволяет просто скопировать нужные части моделей как вложенные классы для миграций.

Дебагер


XDebug очень хорош. Не поймите меня неправильно, но использование точки останова для отладки принесло революцию в мир PHP разработки. Ведь раньше начинающие разработчики были вынуждены отлаживаться с помощью «var_dump» и обновления страницы.

С другой стороны, настройка XDebug для работы с вашей IDE, поиск нужного аддона, модификация php.ini для нужной версии PHP с помощью «end_extension=xdebug.so», отправка информации о точках останова даже если вы используете Vagrand – все это довольно болезненно.

В Ruby другой подход. Так же как при отладке JavaScript в браузере, вы можете использовать ключевое слово «debugger» и получить точку останова. В момент, когда эта строчка кода выполнится, вы получите консоль REPL для отладки вашего кода.

Для отладки доступно несколько дебагеров, к примеру «pry» и «byebug». Оба поставляются в виде гемов и могут быть легко установлены в проект с помощью Bundler и вот такого Gemfile:

group :development, :test do
  gem "byebug"
end

Это является аналогом dev-зависимости Composer, и после установки дебагер будет сразу доступен в Rails. Если же вы не используете рельсы, то достаточно будет выполнить require для «byebug».

В мануале «Debugging Rails Applications» показано, как выглядит использование команды «debugger» в приложении:

[1, 10] in /PathTo/project/app/controllers/articles_controller.rb
    3:
    4:   # GET /articles
    5:   # GET /articles.json
    6:   def index
    7:     byebug
=>  8:     @articles = Article.find_recent
    9:
   10:     respond_to do |format|
   11:       format.html # index.html.erb
   12:       format.json { render json: @articles }
 
(byebug)

Стрелка указывает на строку, которую REPL выполнит в следующий шаг. В этой точке "@articles" еще не определено, но можно вызвать «Article.find_recent» и посмотреть, что происходит в программе. В случае ошибки можно выполнить «next» и выполнить текущую строку, или же «step» и перейти «внутрь» метода на текущей строке (если таковой есть).

Полезные приспособления, особенно если вы пытаетесь понять, почему код ведет себя, как вы его написали, а не как вы хотели. Изучение внутренностей программы и изменение её «на лету» позволяет довольно быстро обнаружить проблему и внести изменения в исходный код.

Делать все это при разработке тестов – бесценно.

Unless


Многим не нравится конструкция «unless». Её часто используют не по назначению. Как видно из этой статьи 2008 года, это недовольство продолжается уже долгие годы.

«unless» является противоположностью «if». Вместо того, чтобы выполнить код, когда уловие верно, код выполняется, когда оно не верно.

 unless foo?
  # делаем что-нибудь полезное
end

# или

def something
  return false unless foo?
  # делаем что-нибудь полезное
end

«unless» полезна в сложных конструкциях с несколькими условиями, объединенными "||" и осложненными скобками. Вместо того, чтобы еще усложнить все отрицанием: «if! (foo || bar) && baz», вы можете воспользоваться «unless» и сделать код чуток проще для чтения (при условии, что читатель хорошо знаком с unless): «unless (foo || bar) && baz».

Конечно, никто в здравом уме не даст разработчику закоммитить «unless», в котором есть «else», но в одиночном исполнении эта конструкция довольно полезна.

Когда разработчики потребовали эту фичу для PHP в 2007 году, предложение долгое время игнорировалось. Пока, наконец, автор PHP Расмус Лердорф не ответил, что это сломает существующий код с функцией «unless» и будет не очевидно для всех, чей родной язык не английский. Например, для него.

Странное слово, на самом деле. Оно означает «в случае если не», но по логике английского языка должно означать «больше». Потому что «less» означает меньше, а приставка «un» добавляет отрицание. Но вот в данном конкретном случае логика не сработала.

Сам я не согласен с такой трактовкой. Если бы программисты цеплялись к таким мелочам, как типовое значение приставок, то «uniqid» точно так же вводило бы в заблуждение, потому что «не-iqid».

Мы высказали это все автору PHP, были обвинены в занудстве, а предложение получило пометку «wontfix».

Методы-предикаты


В мире Ruby есть несколько клевых соглашений, которые для PHP решаются совсем другими способами. Одно из таких соглашений касается методов-предикатов. Напомню, что предикатом называют такой метод или функцию, которая возвращает булевое значение. Так как в Ruby отсутствуют type hints для возвращаемых значений, такое соглашение явно выражает намерение разработчика и делает код более читабельным.

В стандартной библиотеке Ruby имена предикатов заканчиваются вопросительным знаком. Например, «object.nil?», что соответствует "$object === nil" в PHP. или «include?» вместо «inlucde», где знак вопроса явно показывает, что мы задаем вопрос, а не выполняем действие.

Создание собственных предикатов радует красотой кода:

class Rider
  def driver?
    !potential_driver.nil? && vehicle.present?
  end
end

Опытные PHP разработчики именуют такие методы, добавляя к ним приставку «is» или «has», так что в случае PHP у нас будет «isDriver» или «hasVehicle». Но часто такое именование затруднительно и заставляет разработчика тратить ценный фокус внимания на придумывание адекватного имени. К примеру, совершенно понятно, что метод «can_drive?» в Ruby осуществляет проверку. В то же время, для метода «canDirve» в PHP это уже не так понятно. А более понятная версия «isAbleToDrive» страдает излишней длиной и требует от программиста напрячься.

Краткий синтаксис для коллекций


Литеральный конструктор коллекций в PHP довольно прост, а начиная с версии 5.4 упростился еще больше:

// < 5.4
$a = array('one' => 1, 'two' => 2, 'three' => 'three');
// >= 5.4
$a = ['one' => 1, 'two' => 2, 'three' => 'three'];

Но для некоторых программистов и такой синтаксис “слишком многословен”. Начиная с версии 1.9, в Ruby появилась возможность заменить ‘=>’ на точки с запятыми. Если бы такая возможность была в PHP, синтаксис стал бы еще проще:

$a = ['one': 1, 'two': 2, 'three': 'three'];

Хорошая штука, которая становится еще лучше, если вы работаете с вложенными коллекциями.

Sean Coates предложил такой синтаксис для PHP еще в 2011, но убедить авторов языка так и не получилось. Они предпочитаю держать синтаксис PHP минималистичным и отказываются добавлять новые способы решения каких-то задач, если эта задача в принципе может быть решена PHP. Говоря простыми словами, “синтаксический сахар” не является приоритетным для PHP, и в то же время является одним из краеугольных камней Ruby.

Литеральные конструкторы объектов


Предложение, на которое я дал ссылку выше, также подчеркивает одну из фичей Ruby, которую я бы очень хотел видеть в PHP: литералы для объектов. В PHP, если вы хотите создать экземпляр класса «StdCkass» с несколькими полями, у вас есть два варианта:

$esQuery = new stdClass;
$esQuery->query = new stdClass;
$esQuery->query->term = new stdClass;
$esQuery->query->term->name = 'beer';
$esQuery->size = 1;

// или

$esQuery = (object) array(
   "query" => (object) array(
       "term" => (object) array(
           "name" => "beer"
       )
   ),
   "size" => 1
);

Да, в PHP так делали всегда. Но ведь можно делать намного проще!

Sean Coates предложил вот такой синтаксис, практически полностью повторяющий Ruby:

PHP

$esQuery = {
   "query" : {
       "term" : {
           "name" : "beer"
       }
   },
   "size" : 1
};
Ruby

esQuery = {
   "query" : {
       "term" : {
           "name" : "beer"
       }
   },
   "size" : 1
}

Я бы очень-очень хотел чтобы синтаксис был добавлен в PHP. Но, увы, это уже предлагалось и было встречено безо всякого интереса со стороны разработчиков.

Rescue в методе


Вместо «try/catch» как в PHP, Ruby использует «begin/rescur». Работают они практически одинаково, с учетом того, что в версии 5.6 PHP был добавлен «finally».

И PHP, и Ruby позволяют поймать исключение в любом месте программы. Но Ruby предлагает любопытный синтаксис: «begin» можно не писать, просто указав «rescue» в теле метода, тем самым деля его на две части, «нормальное выполнение» и «обработка ошибок»:

Ruby


def create(params)
  do_something_complicated(params)
  true
rescue SomeException
  false
end

Этот простой синтаксист подталкивает разработчиков к более вдумчивой работы с ошибками, вместо популярного подхода «выбрасываем все на самый верх, а там взрываемся в логгер».

Если бы я решил сделать что-то подобное для PHP, то синтаксис бы выглядел примерно так:

function create($params) {
  do_something_complicated($params);
  return true;
} catch (SomeException $e) {
  return false;
}

Конечно, это не самая важная штука. Но именно любовь к мелочам делает Ruby языком, на котором приятно разрабатывать. Создается впечатление, что язык пытается помочь тебе в решении каждодневных программерских задач.

Повторная попытка после исключения


Несколько месяцев назад я обнаружил очень полезную штуку в Ruby, которую раньше почему-то не замечал. Ключевое слово retry:

begin
  SomeModel.find_or_create_by(user: user)
rescue ActiveRecord::RecordNotUnique
  retry
end

В этом примере случается типичный race condition. Возможно, потому что «find_or_create_by» не атомарно (ORM делает SELECT, а затем INSERT вне транзакции). И, если вам не повезло, запись будет создана параллельным процессом после срабатывания SELECT но до INSERT.

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

«Честно» делать повторную попытку — это писать усложненный код. А Ruby позволяет делать вот так:

def upload_image
  begin
    obj = s3.bucket('bucket-name').object('key')
    obj.upload_file('/path/to/source/file')
  rescue AWS::S3::UploadException
    retry
  end
end

В случае PHP нам придется для решения такой же задачи сильно увеличить количество «вспомогательного» кода:

function upload_image($path) {
  $attempt = function() use ($path) {
    $obj = $this->s3->bucket('bucket-name')->object('key');
    $obj->upload_file($path);
  };
  
  try {
    $attempt();
  } catch (AWS\S3\UploadException $e)
    $attempt();
  }
}

Конечно, в реальной жизни неплохо будет добавить еще и защиту от зацикливания. Но, в любом случае, вы сами видите, насколько лаконичнее такой код получается в Ruby.

Маленькая птичка чирикнула мне, что похожая фича разрабатывается для PHP и, надеюсь, будет скоро объявлена. Теоретически, она может появиться даже в PHP 7.1, если нам сильно повезет.

Заключение


Первое время я использовал Ruby как PHP. Совместная работа с командой потрясающих Ruby хакеров научила меня многим «рубизмам» и лучшим практикам языка.

Эта статья описывает то, что мне понравилось в Ruby по сравнению с PHP. О чем я буду скучать, если мне понадобиться снова на нем разрабатывать. Но, тем не менее, это не остановит меня, если такая необходимость возникнет. Хейтеры PHP игнорируют тот факт, что язык постоянно развивается – посмотрите на огромный прогресс, который произошел в версиях PHP 7 и PHP 7.1!

PHP делает упор на целостность языка, единообразный синтаксис для переменных, контекстно-чувствительный лексер и AST. Все это делает PHP более цельным языком, несмотря на косяки в стандартной библиотеке.

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

Другие языки могут развиваться во всех направлениях, пробовать новые теории, проводить смелые эксперименты и поражать аудиторию Hacker News. После чего самые удачные решения будет заимствованы PHP. И мне нравится такой подход. В конце концов, PHP – это делающий набеги пират в мире языков программирования.

Для программиста очень полезно пробовать разные языки программирования. Ruby в этом плане очень хорош. Также неплох Golang, а в Elixir есть много интересных решений, хотя и не без косяков. Просто не пытайтесь использовать все это сразу в продакшн, а используйте, чтобы стать лучшим программистом.
Поделиться с друзьями
-->

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