Saturday, February 26, 2011

Ruby: пообезьянничаем? ;-)

В некоторых, так скажем, «математико-ориентированных» языках программирования операции над векторами реализуются довольно компактным образом. В качестве примеров таких языков можно привести J и R.

В 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]

2 comments:

  1. require 'mathn'
    V = Vector
    V[1,2,3] * 5

    ReplyDelete
  2. Ну... Во-первых, тогда уж require 'matrix' — зачем весь mathn тянуть? Во-вторых, там векторы чисто в смысле линейной алгебры, т.е. из определённых операций только умножение на число и сложение с вычитанием есть. А тут в несколько более широком смысле векторы рассматриваются.

    Примеры подправил, дабы иллюзий не возникало)

    ReplyDelete