В R вектор создаётся функцией c — это сокращение от 'concatenate'. Поскольку создание вектора — весьма частое явление, сократили название до предела. Компактность операций заключается в следующем:
> c(1, 2, 3) ** 3 [1] 1 8 27 > c(1, 2, 3) * c(5, 3, 1) [1] 5 6 3 > log10(c(1, 10, 100, 1000)) [1] 0 1 2 3
Думаю, идея ясна.
Можно ли что-нибудь подобное реализовать в Ruby? Запросто! Чтобы вышеприведённые примеры работали, потребуется всего-то порядка 30 строк кода.
Для начала унаследуемся от
Array
, чтобы не изменить ненароком поведение уже существующего кода:class Vector < Array endИ создадим ту самую функцию c:
def c(*args) Vector.new(args) end*args здесь обозначает произвольное число параметров; args — массив из них. Конструктор
new
унаследовался, так что особо трудиться не понадобилось.Теперь перейдём к арифметическим действиям. Для начала надо бы разопределить оставшиеся от
Array
операторы +, - и т. п., коли уж мы вознамерились придать им совершенно иной смысл:class Vector < Array METHODS_TO_UNDEFINE = %w[+ - * == << & | ! != <=>].map(&:to_sym) METHODS_TO_UNDEFINE.each { |sym| undef_method sym } endЗдесь %w[word1 word2 ...] ≡ ['word1', 'word2', ...]. Потом весьма желательно
map
-ом перелопатить массив строк в массив символов. (Грубо говоря, символы в Ruby — это почти что строки, но при виде символа интерпретатор не стремится создавать новую строку в памяти, а сперва ищет символ в таблице уже существующих, которая отображает символы в их целочисленные идентификаторы. Потому скорость обработки символов выше.)Ну а далее просто вызываем приватный метод
undef_method
класса Vector
для каждого из этих символов.Теперь есть два пути.
Первый — определить все нужные операции с помощью
define_method
. Второй — воспользоваться тем, что в случае ненахождения метода Ruby вызывает метод
method_missing
, передавая ему в качестве аргументов символ, соответствующий отсутствующему методу, и переданные тому методу аргументы (а также, возможно, блок).Пойдём вторым путём. Поведение
method_missing
будет различным в двух случаях:1) если передаётся вектор той же длины, что и наш, будем применять операцию попарно к элементам двух векторов;
2) в противном случае просто будем применять операцию к каждому из элементов нашего вектора.
Итого получаем:
def method_missing(m, *args, &b) return [] if self.empty? if args.length == 1 and args[0].class == Vector and args[0].length == self.length then Vector.new(length) { |i| self[i].send m, args[0][i], &b } else Vector.new(length) { |i| self[i].send m, *args, &b } end end
Унаследованный конструктор
new
в таком виде сразу выделяет память для length
элементов и присваивает i
-му элементу то, что для его позиции возвращает переданный конструктору блок.Про
send
я когда-то уже писал, так что повторяться не буду. Здесь, возможно, нужно отметить использование *
в ветке 'else': звёздочка перед именем массива «разворачивает» этот массив в набор элементов, разделённых запятыми. Ну и наконец, надо бы добавить всякие математические функции, чтобы можно было писать что-нибудь типа
sqrt(c(1,4,9))
. Тут-то мы и воспользуемся define_method
(это такой же приватный метод класса Vector
, как и undef_method
).Для начала неплохо бы понять, что же вообще определять.
Предложение: возьмём все функции из модуля
Math
, которые могут принимать один аргумент, и преобразуем их в то, что нам нужно. Это делается просто: Math.methods(false).select { |m| Math.method(m).arity == 1 }
. (Опциональный аргумент false
здесь нужен для того, чтобы не включать методы, унаследованные от Object.) Ещё в это множество неплохо бы натуральный логарифм добавить (log
по понятной причине может принимать два аргумента, а потому имеет arity == -1
).Ну а про определение методов и рассказывать нечего:
module Kernel Math.methods(false).select { |m| Math.method(m).arity == 1 } .push(:log).each { |func| define_method(func) { |arg| if arg.class == Vector then Vector.new(arg.length) { |i| Math.send func, arg[i] } else Math.send func, arg end } private(func) } end
Ну вот и всё :-)
print sqrt( c(1, 2, 3) ** (c(1, 2, 3) - 1) ) # => [1.0, 1.4142135623730951, 3.0]
require 'mathn'
ReplyDeleteV = Vector
V[1,2,3] * 5
Ну... Во-первых, тогда уж require 'matrix' — зачем весь mathn тянуть? Во-вторых, там векторы чисто в смысле линейной алгебры, т.е. из определённых операций только умножение на число и сложение с вычитанием есть. А тут в несколько более широком смысле векторы рассматриваются.
ReplyDeleteПримеры подправил, дабы иллюзий не возникало)