Tuesday, September 20, 2011

Этот безумный R


Сначала мне казалось, что это простой и примитивный язык, для небольших скриптов, с уклоном в статистику. Потом мне стало казаться, что это самый обычный язык программирования, с уклоном в статистику. Сейчас мне кажется, что это гремучая смесь классических императивных языков вроде C++ или Java и матерой функциональщины вроде Haskell или Lisp. И всё это R.

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

Векторизация

Векторизация это первое с чем сталкиваются все R разработчики не всегда, правда, осознанно. Векторы, в R повсюду. Даже когда мы просто объявляем в консоли число 1, мы создаем вектор с единственным элементом.

> 1
[1] 1

Как следствие этого, большинство функций в R работают с векторами. И обычно об этом даже не нужно думать. Например:
> c(1,2,3)+c(4,5,6)
[1] 5 7 9
> c(1,2,3)*c(4,5,6)
[1]  4 10 18

Чертовски удобно, когда задача сводится к обработке больших массивов однотипных данных. Что, в общем-то, является типичной задачей в статистике.

Собственные функции тоже часто получаются векторизованными сами собой. Но не всегда. Например конструкция if не векторизована, но зато есть векторизованная конструкция ifelse которую и рекомендуется использовать в большинстве случаев:

> a <- c(1,-2,-3,4)
> ifelse(a>0, 1, 0)
[1] 1 0 0 1

Recycling rule

Эта концепция важна в сочетании с векторизацией. Что будет, если в примере выше два вектора будут разной длины? Оказывается, что если размер более длинного вектора кратен размеру короткого, то короткий вектор будет автоматически повторен нужное количество раз. Если же не кратен, то произойдет то же самое, но будет показано предупреждение (которое, правда, можно подавить)

> c(1,2)+c(1,2,3,4)
[1] 2 4 4 6
> c(1,2,3)+c(1,2,3,4)
[1] 2 4 6 5
Предупреждение
In c(1, 2, 3) + c(1, 2, 3, 4) :
  длина большего объекта не является произведением длины меньшего объекта

Это очень удобное свойство, но главное его удобство проявляется неявно. Например, когда мы хотим все значения в векторе увеличить на единицу:
> c(1,2,3)+1
[1] 2 3 4
Мы ведь помним, что единица это тоже вектор, с длиной 1.

Всё является функцией

Да, R действительно функциональный язык программирования. Функцией является всё, даже скобочки {, [, операторы присвоения <- и условные операторы if, while и т.п. Даже ключевое слово, объявляющее функции function - тоже является функцией. И главное, их можно переопределить!

> `if`
.Primitive("if")
> `if` <- function(...) {"Hello"}
> if (TRUE) 1 else 0
[1] "Hello"
> `if` <- .Primitive("if")
> if (TRUE) 1 else 0
[1] 1

Для чего это может понадобиться не берусь даже представить.

Отложенные вычисления


R, как и Haskell, использует концепцию отложенных (lazy) вычислений. Это значит, что аргументы функций не вычисляются до тех пор пока их значения не потребуются. На самом деле, незаметно для программиста, R передает в функции специальные объекты "обещания" (promise), эти объекты содержат в себе всё, что необходимо, чтобы по требованию вычислить значение. Если значение было вычислено, то оно запоминается в объекте promise и используется в дальнейшем по мере необходимости.

> fun <- function(x,a,b) {if (x) a else b}
> fun(TRUE, 1, factorial(10000))
[1] 1
> fun(FALSE, 1, factorial(10000))
[1] Inf
Предупреждение
In factorial(10000) : значение в 'gammafn' -- за пределами

Как видно из примера, в первом вызове функции fun значение факториала даже не вычислялось.

Окружение


Окружение (environment) это часть механизма замыканий (closures), реализованного в R. Любая переменная доступная в окружении на момент вызова функции сохраняется и передается вместе с окружением в вызываемую функцию. Но самое неожиданное, это то, что с помощью функции assign можно определить некую переменную не только в текущем окружении, но и в родительском, например так:

> foo <- function(){assign("a", 123, envir=parent.frame())}
> bar <- function(){foo(); message(a)}
> bar()
123

Зачем это может быть нужно, мне сложно представить. В любом случае, полагаю, что использовать эту возможность следует с большой осторожностью. В целом, язык программирования R оказался намного больше, разнообразней и сложнее чем казалось в начале. Здесь я перечислил далеко не все его возможности. Более полное описание языка можно найти в его спецификации.

1 comment:

  1. Окружение приятно использовать, если что-то не работает из-за неочевидных проблем с данными. Делаем из пострадавшей функции assign() в globalenv() перед пострадавшей операцией. Запускаем, всё падает. Идём в консоль и разглядываем данные, которые привели к падению.

    ReplyDelete