Каковы соглашения для типов в Ruby?

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

Кроме того, когда речь идет о написании проектных документов на основе кода Ruby, каким будет правильный способ указать, над какими типами должен работать метод? Например, Javadocs (хотя обычно они не используются для проектных документов) точно определяют, над какими типами будет работать метод, поскольку сам язык статически типизирован, но кажется, что документы Ruby всегда очень неточны в предварительных и постусловиях методов., Существует ли стандартная практика для определения такого формата в Ruby?

4 ответа

Первое, что вам нужно знать, это различие между классами и типами.

Очень жаль, что Java смешивает это различие, поскольку классы всегда являются типами (хотя в Java есть и другие типы, которые не являются классами, то есть интерфейсы, примитивы и параметры универсальных типов). Фактически, почти каждая книга о стиле Java скажет вам не использовать классы как типы. Кроме того, в своей основополагающей работе " О понимании абстракции данных", вновь посещаемой, Уильям Р. Кук отмечает, что в Java классы описывают абстрактные типы данных, а не объекты. Интерфейсы описывают объекты, поэтому, если вы используете классы как типы в Java, вы не делаете OO; если вы хотите использовать OO в Java, единственное, что вы можете использовать в качестве типов, - это интерфейсы, и единственное, для чего вы можете использовать классы, - это как фабрики.

В Ruby типы больше похожи на сетевые протоколы: тип описывает сообщения, которые объект понимает, и то, как он реагирует на них. (Это сходство не случайно: Smalltalk, далекий предок Руби, был вдохновлен тем, что впоследствии стало Интернетом. На языке Smalltalk "протокол" - это термин, который неофициально используется для описания типов объектов. В Objective-C этот неформальный Понятие протокола стало частью языка, и Java, на которую в первую очередь повлиял Objective-C, напрямую скопировала это понятие, но переименовала его в "интерфейс".)

Итак, в Ruby мы имеем:

  • module (языковая особенность): средство для совместного использования кода и дифференциальной реализации; не тип
  • class (языковая особенность): фабрика для объектов, также IS-A moduleне тип
  • протокол (неформальная вещь): тип объекта, характеризуемый сообщениями, на которые реагирует и как он реагирует на них

Также обратите внимание, что объект может иметь более одного типа. Например, строковый объект имеет оба типа "Appendable" (он отвечает на <<) и "индексируемый" (он отвечает на []).

Итак, резюмируем важные моменты:

  • типы не существуют в языке Ruby, только в голове программиста
  • классы и модули не являются типами
  • типы - это протоколы, характеризуемые тем, как объект отвечает на сообщения

Очевидно, что протоколы не могут быть указаны на языке, поэтому они обычно указываются в документации. Хотя чаще всего они не указаны вообще. На самом деле это не так плохо, как кажется: часто требования, предъявляемые к аргументам отправляемого сообщения, например, "очевидны" из названия или предполагаемого использования метода. Кроме того, в некоторых проектах ожидается, что пользовательские приемочные тесты будут выполнять эту роль. (Это имело место, например, в более не существующей веб-инфраструктуре Merb. API был полностью описан в приемочных тестах.) Сообщения об ошибках и исключения, которые вы получаете при передаче неправильного типа, также часто достаточно, чтобы выяснить, какой метод требует. И последнее, но не менее важное: всегда есть исходный код.

Есть несколько известных протоколов, таких как each протокол, который требуется путем смешивания в Enumerable (объект должен отвечать each уступая его элементы один за другим и возвращая self если блок передан и возвращает Enumerator если блок не пропущен), Range протокол, который требуется, если объект хочет быть конечной точкой Range (оно должно отвечать succ со своим преемником, и он должен ответить на <=), или <=> протокол требуется путем смешивания в Comparable (объект должен отвечать <=> либо с -1, 0, 1, или же nil). Они также нигде не записаны, или только фрагментарно, ожидается, что они будут хорошо известны существующим рубистам и хорошо обучены новым.

Хороший пример StringIO: он имеет тот же протокол, что и IO но не наследуют от него и не наследуют от общего предка (за исключением очевидного Object). Итак, когда кто-то проверяет IOЯ не могу пройти в StringIO (очень полезно для тестирования), но если бы они просто использовали объект AS-IF, это было бы IOЯ могу пройти в StringIOи они никогда не узнают разницу.

Конечно, это не идеально, но сравните это с Java: многие важные требования и гарантии также указаны в прозе! Например, где в типе подпись List.sort это говорит о том, что результирующий список будет отсортирован? Нигде! Это упоминается только в JavaDoc. Какой тип функционального интерфейса? Опять же, указана только английская проза. Stream API имеет целый набор концепций, которые не отражаются в системе типов, таких как невмешательство и изменчивость.

Я прошу прощения за это длинное эссе, но очень важно понять разницу между классом и типом, а также понять, что такое тип в ОО-языке, таком как Ruby.

Лучший способ работы с типами - просто использовать объект и документировать протокол. Если вы хотите позвонить, просто позвоните call; не требуйте, чтобы это было Proc, (Во-первых, это означало бы, что я не могу пройти Method, что было бы раздражающим ограничением.) Если вы хотите что-то добавить, просто позвоните +, если вы хотите что-то добавить, просто позвоните <<, если вы хотите что-то напечатать, просто позвоните print или же puts (последний вариант полезен, например, при тестировании, когда я могу просто передать StringIO вместо File). Не пытайтесь программно определить, удовлетворяет ли объект определенному протоколу, это бесполезно: это эквивалентно решению проблемы остановки. Система документации YARD имеет тег для описания типов. Это полностью произвольный текст. Тем не менее, есть предложенный язык типов (который мне не особенно нравится, потому что я думаю, что он слишком сосредоточен на классах, а не на протоколах).

Если вы действительно должны иметь экземпляр определенного класса (в отличие от объекта, который удовлетворяет определенному протоколу), в вашем распоряжении есть несколько методов преобразования типов. Однако обратите внимание, что как только вам требуются определенные классы вместо того, чтобы полагаться на протоколы, вы покидаете сферу объектно-ориентированного программирования.

Наиболее важные методы преобразования типов, которые вы должны знать, это однобуквенные и многобуквенные to_X методы. Вот важное различие между ними:

  • если объект может "несколько разумно" быть представлен в виде массива, строки, целого числа, числа с плавающей запятой и т. д., на который он будет реагировать to_a, to_s, to_i, to_f, так далее.
  • если объект того же типа, что и экземпляр Array, String, Integer, Floatи т. д. он будет реагировать на to_ary, to_str, to_int, to_float, так далее.

Для обоих этих методов гарантируется, что они никогда не вызовут исключение. (Если они вообще существуют, конечно, в противном случае NoMethodError будет повышен.) Для обоих этих методов гарантируется, что возвращаемое значение будет экземпляром соответствующего базового класса. Для многобуквенных методов преобразование должно быть семантически без потерь. (Обратите внимание, когда я говорю "это гарантировано", я имею в виду уже существующие методы. Если вы пишете свой собственный, это не гарантия, а требование, которое вы должны выполнить, чтобы оно стало гарантией для других, использующих ваш метод.)

Многосимвольные методы обычно намного строже, и их гораздо меньше. Например, совершенно разумно сказать, что nil "можно представить как" пустую строку, но было бы смешно сказать, что nil IS-AN пустая строка, поэтому nil отвечает на to_s, но нет to_str, Аналогично, поплавок реагирует на to_i возвращая свое усечение, но оно не отвечает to_intпотому что вы не можете без потерь преобразовать число с плавающей точкой в ​​целое число.

Вот один пример из Ruby API: Arrayна самом деле не реализуются с использованием принципов ОО. Рубиновые читы, по соображениям производительности. В результате вы можете действительно только индексировать в Array с фактическим экземпляром Integer класс, а не с любым произвольным "целочисленным" объектом. Но вместо того, чтобы требовать, чтобы вы прошли в IntegerРуби позвонит to_int Во-первых, чтобы дать вам возможность по-прежнему использовать ваши собственные целочисленные объекты. Не вызывает to_iоднако, поскольку нет смысла индексировать массив с чем-то, что не является целым числом; это можно только "несколько разумно представить" как единое целое. Ото, Kernel#print, Kernel#puts, IO#print, IO#putsи друзья звонят to_s на их аргументы, чтобы позволить вам иметь любой объект, который будет разумно напечатан. А также Array#join звонки to_str на его аргумент, но to_s на элементах массива; как только вы поймете, почему это имеет смысл, вы намного ближе к пониманию типов в Ruby.

Вот несколько практических правил:

  • не проверяйте типы, просто используйте их и задокументируйте
  • если вы абсолютно положительно ДОЛЖНЫ иметь экземпляр определенного класса, вам, вероятно, следует использовать многобуквенные преобразования типов; не просто тестируйте для класса, дайте объекту возможность преобразовать себя
  • однобуквенные преобразования типов почти всегда неправильны, кроме to_s для печати; сколько ситуаций вы можете себе представить, где молча конвертировать nil или же "one hundred" в 0 без вас даже не понимая, что есть nil или строка правильная вещь?

ИМО это довольно основано на мнении. И сильно зависит от контекста и ваших требований. Спросите себя: меня это волнует? Можно ли поднять ошибку? Кто является пользователем (мой код против внешних клиентов)? Могу ли я справиться с фиксацией ввода?

Я думаю, что все в порядке от пофиг (может вызвать странные исключения)

def add(a, b)
  a + b # raise NoMethodError if a does not respond_to +
end

над использованием проверок типа утки

def add(a, b)
  if a.respond_to?(:+)
     a + b
  else
     "#{a} #{b}" # might makes sense?
  end
end 

или просто переведите его в исключенный тип

def add(a, b)
  a.to_i + b.to_i
end

чтобы проверить тип заранее (и вызвать полезное исключение):

def integers(a, b)
  raise ArgumentError, "args must be integers" unless a.is_a?(Integer) and b.is_a?(Integer)
  a + b
end

Это действительно зависит от ваших потребностей и уровня безопасности, в которой вы нуждаетесь.

Интересный вопрос!

Тип-безопасности

Java и Ruby в значительной степени диаметрально противоположны. В Ruby вы можете сделать:

String = Array
# warning: already initialized constant String
p String.new
# []

Таким образом, вы можете в значительной степени забыть о безопасности типов, которую вы знаете из Java.

По первому вопросу вы можете:

  • убедитесь, что метод не вызывается ни с чем, кроме Integer (например, my_method(array.size))
  • принять, что метод может вызываться с помощью Float, Integer или Rational и, возможно, вызывать to_i на входе.
  • используйте методы, которые хорошо работают с Floats: например (1..3.5).to_a #=> [1, 2, 3], 'a'*2.5 #=> 'aa'
  • если он вызывается с чем-то еще, вы можете получить NoMethodError: undefined method 'to_i' for object ... и вы можете попытаться справиться с этим (например, с rescue)

Документация

Первым шагом документирования ожидаемого ввода и вывода ваших методов будет определение метода в правильном месте (класс или модуль) и использование соответствующих имен методов:

  • is_prime? должен вернуть логическое значение
  • is_prime? должны быть определены в Integer

В противном случае YARD поддерживает типы в документации:

# @param [Array<String, Symbol>] arg takes an Array of Strings or Symbols
def foo(arg)
end

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

Другие вопросы по тегам