Каковы соглашения для типов в 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-Amodule
не тип- протокол (неформальная вещь): тип объекта, характеризуемый сообщениями, на которые реагирует и как он реагирует на них
Также обратите внимание, что объект может иметь более одного типа. Например, строковый объект имеет оба типа "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
Я не уверен в том, почему вы должны требовать, чтобы в ваш метод передавались только целые числа, но я бы не стал активно проверять в моем коде, что значение является целым числом. Например, если вы выполняете арифметику, для которой требуется целое число, я бы типизировал или преобразовал значение в целое число в той точке, где это необходимо, и объяснил с помощью комментариев или в заголовке вашего метода цель для этого.