ob-R.el (20199B)
1 ;;; ob-R.el --- Babel Functions for R -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2009-2024 Free Software Foundation, Inc. 4 5 ;; Author: Eric Schulte 6 ;; Dan Davison 7 ;; Maintainer: Jeremie Juste <jeremiejuste@gmail.com> 8 ;; Keywords: literate programming, reproducible research, R, statistics 9 ;; URL: https://orgmode.org 10 11 ;; This file is part of GNU Emacs. 12 13 ;; GNU Emacs is free software: you can redistribute it and/or modify 14 ;; it under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation, either version 3 of the License, or 16 ;; (at your option) any later version. 17 18 ;; GNU Emacs is distributed in the hope that it will be useful, 19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 ;; GNU General Public License for more details. 22 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 25 26 ;;; Commentary: 27 28 ;; Org-Babel support for evaluating R code 29 30 ;;; Code: 31 32 (require 'org-macs) 33 (org-assert-version) 34 35 (require 'cl-lib) 36 (require 'ob) 37 38 (declare-function orgtbl-to-tsv "org-table" (table params)) 39 (declare-function run-ess-r "ext:ess-r-mode" (&optional start-args)) 40 (declare-function inferior-ess-send-input "ext:ess-inf" ()) 41 (declare-function ess-make-buffer-current "ext:ess-inf" ()) 42 (declare-function ess-eval-buffer "ext:ess-inf" (vis)) 43 (declare-function ess-wait-for-process "ext:ess-inf" 44 (&optional proc sec-prompt wait force-redisplay)) 45 46 (defconst org-babel-header-args:R 47 '((width . :any) 48 (height . :any) 49 (bg . :any) 50 (units . :any) 51 (pointsize . :any) 52 (antialias . :any) 53 (quality . :any) 54 (compression . :any) 55 (res . :any) 56 (type . :any) 57 (family . :any) 58 (title . :any) 59 (fonts . :any) 60 (version . :any) 61 (paper . :any) 62 (encoding . :any) 63 (pagecentre . :any) 64 (colormodel . :any) 65 (useDingbats . :any) 66 (horizontal . :any) 67 (async . ((yes no))) 68 (results . ((file list vector table scalar verbatim) 69 (raw html latex org code pp drawer) 70 (replace silent none append prepend) 71 (output value graphics)))) 72 "R-specific header arguments.") 73 74 (defconst ob-R-safe-header-args 75 (append org-babel-safe-header-args 76 '(:width :height :bg :units :pointsize :antialias :quality 77 :compression :res :type :family :title :fonts 78 :version :paper :encoding :pagecentre :colormodel 79 :useDingbats :horizontal)) 80 "Header args which are safe for R babel blocks. 81 82 See `org-babel-safe-header-args' for documentation of the format of 83 this variable.") 84 85 (defvar org-babel-default-header-args:R '()) 86 (put 'org-babel-default-header-args:R 'safe-local-variable 87 (org-babel-header-args-safe-fn ob-R-safe-header-args)) 88 89 (defcustom org-babel-R-command "R --slave --no-save" 90 "Name of command to use for executing R code." 91 :group 'org-babel 92 :version "24.1" 93 :type 'string) 94 95 ;; The usage of utils::read.table() ensures that the command 96 ;; read.table() can be found even in circumstances when the utils 97 ;; package is not in the search path from R. 98 (defconst ob-R-transfer-variable-table-with-header 99 "%s <- local({ 100 con <- textConnection( 101 %S 102 ) 103 res <- utils::read.table( 104 con, 105 header = %s, 106 row.names = %s, 107 sep = \"\\t\", 108 as.is = TRUE 109 ) 110 close(con) 111 res 112 })" 113 "R code used to transfer a table defined as a variable from org to R. 114 115 This function is used when the table contains a header.") 116 117 (defconst ob-R-transfer-variable-table-without-header 118 "%s <- local({ 119 con <- textConnection( 120 %S 121 ) 122 res <- utils::read.table( 123 con, 124 header = %s, 125 row.names = %s, 126 sep = \"\\t\", 127 as.is = TRUE, 128 fill = TRUE, 129 col.names = paste(\"V\", seq_len(%d), sep =\"\") 130 ) 131 close(con) 132 res 133 })" 134 "R code used to transfer a table defined as a variable from org to R. 135 136 This function is used when the table does not contain a header.") 137 138 (defun org-babel-expand-body:R (body params &optional _graphics-file) 139 "Expand BODY according to PARAMS, return the expanded body." 140 (mapconcat 'identity 141 (append 142 (when (cdr (assq :prologue params)) 143 (list (cdr (assq :prologue params)))) 144 (org-babel-variable-assignments:R params) 145 (list body) 146 (when (cdr (assq :epilogue params)) 147 (list (cdr (assq :epilogue params))))) 148 "\n")) 149 150 (defun org-babel-execute:R (body params) 151 "Execute a block of R code BODY according to PARAMS. 152 This function is called by `org-babel-execute-src-block'." 153 (save-excursion 154 (let* ((result-params (cdr (assq :result-params params))) 155 (result-type (cdr (assq :result-type params))) 156 (async (org-babel-comint-use-async params)) 157 (session (org-babel-R-initiate-session 158 (cdr (assq :session params)) params)) 159 (graphics-file (and (member "graphics" (assq :result-params params)) 160 (org-babel-graphical-output-file params))) 161 (colnames-p (unless graphics-file (cdr (assq :colnames params)))) 162 (rownames-p (unless graphics-file (cdr (assq :rownames params)))) 163 (full-body 164 (let ((inside 165 (list (org-babel-expand-body:R body params graphics-file)))) 166 (mapconcat 'identity 167 (if graphics-file 168 (append 169 (list (org-babel-R-construct-graphics-device-call 170 graphics-file params)) 171 inside 172 (list "},error=function(e){plot(x=-1:1, y=-1:1, type='n', xlab='', ylab='', axes=FALSE); text(x=0, y=0, labels=e$message, col='red'); paste('ERROR', e$message, sep=' : ')}); dev.off()")) 173 inside) 174 "\n"))) 175 (result 176 (org-babel-R-evaluate 177 session full-body result-type result-params 178 (or (equal "yes" colnames-p) 179 (org-babel-pick-name 180 (cdr (assq :colname-names params)) colnames-p)) 181 (or (equal "yes" rownames-p) 182 (org-babel-pick-name 183 (cdr (assq :rowname-names params)) rownames-p)) 184 async))) 185 (if graphics-file nil result)))) 186 187 (defun org-babel-prep-session:R (session params) 188 "Prepare SESSION according to the header arguments specified in PARAMS." 189 (let* ((session (org-babel-R-initiate-session session params)) 190 (var-lines (org-babel-variable-assignments:R params))) 191 (org-babel-comint-in-buffer session 192 (mapc (lambda (var) 193 (end-of-line 1) (insert var) (comint-send-input nil t) 194 (org-babel-comint-wait-for-output session)) 195 var-lines)) 196 session)) 197 198 (defun org-babel-load-session:R (session body params) 199 "Load BODY into SESSION." 200 (save-window-excursion 201 (let ((buffer (org-babel-prep-session:R session params))) 202 (with-current-buffer buffer 203 (goto-char (process-mark (get-buffer-process (current-buffer)))) 204 (insert (org-babel-chomp body))) 205 buffer))) 206 207 ;; helper functions 208 209 (defun org-babel-variable-assignments:R (params) 210 "Return list of R statements assigning the block's variables. 211 Retrieve variables from PARAMS." 212 (let ((vars (org-babel--get-vars params))) 213 (mapcar 214 (lambda (pair) 215 (org-babel-R-assign-elisp 216 (car pair) (cdr pair) 217 (equal "yes" (cdr (assq :colnames params))) 218 (equal "yes" (cdr (assq :rownames params))))) 219 (mapcar 220 (lambda (i) 221 (cons (car (nth i vars)) 222 (org-babel-reassemble-table 223 (cdr (nth i vars)) 224 (cdr (nth i (cdr (assq :colname-names params)))) 225 (cdr (nth i (cdr (assq :rowname-names params))))))) 226 (number-sequence 0 (1- (length vars))))))) 227 228 (defun org-babel-R-quote-tsv-field (s) 229 "Quote field S for export to R." 230 (if (stringp s) 231 (concat "\"" (mapconcat 'identity (split-string s "\"") "\"\"") "\"") 232 (format "%S" s))) 233 234 (defun org-babel-R-assign-elisp (name value colnames-p rownames-p) 235 "Construct R code assigning the elisp VALUE to a variable named NAME." 236 (if (listp value) 237 (let* ((lengths (mapcar 'length (cl-remove-if-not 'listp value))) 238 (max (if lengths (apply 'max lengths) 0)) 239 (min (if lengths (apply 'min lengths) 0))) 240 ;; Ensure VALUE has an orgtbl structure (depth of at least 2). 241 (unless (listp (car value)) (setq value (mapcar 'list value))) 242 (let ((file (orgtbl-to-tsv value '(:fmt org-babel-R-quote-tsv-field))) 243 (header (if (or (eq (nth 1 value) 'hline) colnames-p) 244 "TRUE" "FALSE")) 245 (row-names (if rownames-p "1" "NULL"))) 246 (if (= max min) 247 (format ob-R-transfer-variable-table-with-header 248 name file header row-names) 249 (format ob-R-transfer-variable-table-without-header 250 name file header row-names max)))) 251 (cond ((integerp value) (format "%s <- %s" name (concat (number-to-string value) "L"))) 252 ((floatp value) (format "%s <- %s" name value)) 253 ((stringp value) (format "%s <- %S" name (org-no-properties value))) 254 (t (format "%s <- %S" name (prin1-to-string value)))))) 255 256 257 (defvar ess-current-process-name) ; dynamically scoped 258 (defvar ess-local-process-name) ; dynamically scoped 259 (defvar ess-ask-for-ess-directory) ; dynamically scoped 260 (defvar ess-gen-proc-buffer-name-function) ; defined in ess-inf.el 261 (defun org-babel-R-initiate-session (session params) 262 "Create or return the current R SESSION buffer. 263 Use PARAMS to set default directory when creating a new session." 264 (unless (string= session "none") 265 (let* ((session (or session "*R*")) 266 (ess-ask-for-ess-directory 267 (and (boundp 'ess-ask-for-ess-directory) 268 ess-ask-for-ess-directory 269 (not (cdr (assq :dir params))))) 270 ;; Make ESS name the process buffer as SESSION. 271 (ess-gen-proc-buffer-name-function 272 (lambda (_) session))) 273 (if (org-babel-comint-buffer-livep session) 274 session 275 (save-window-excursion 276 (when (get-buffer session) 277 ;; Session buffer exists, but with dead process 278 (set-buffer session)) 279 (org-require-package 'ess-r-mode "ESS") 280 (set-buffer (run-ess-r)) 281 (let ((R-proc (get-process (or ess-local-process-name 282 ess-current-process-name)))) 283 (while (process-get R-proc 'callbacks) 284 (ess-wait-for-process R-proc))) 285 (current-buffer)))))) 286 287 (defun org-babel-R-associate-session (session) 288 "Associate R code buffer with an R session. 289 Make SESSION be the inferior ESS process associated with the 290 current code buffer." 291 (when-let* ((process (get-buffer-process session))) 292 (setq ess-local-process-name (process-name process)) 293 (ess-make-buffer-current)) 294 (setq-local ess-gen-proc-buffer-name-function (lambda (_) session))) 295 296 (defvar org-babel-R-graphics-devices 297 '((:bmp "bmp" "filename") 298 (:jpg "jpeg" "filename") 299 (:jpeg "jpeg" "filename") 300 (:tikz "tikz" "file") 301 (:tiff "tiff" "filename") 302 (:png "png" "filename") 303 (:svg "svg" "file") 304 (:pdf "pdf" "file") 305 (:ps "postscript" "file") 306 (:postscript "postscript" "file")) 307 "An alist mapping graphics file types to R functions. 308 309 Each member of this list is a list with three members: 310 1. the file extension of the graphics file, as an elisp :keyword 311 2. the R graphics device function to call to generate such a file 312 3. the name of the argument to this function which specifies the 313 file to write to (typically \"file\" or \"filename\")") 314 315 (defun org-babel-R-construct-graphics-device-call (out-file params) 316 "Construct the call to the graphics device." 317 (let* ((allowed-args '(:width :height :bg :units :pointsize 318 :antialias :quality :compression :res 319 :type :family :title :fonts :version 320 :paper :encoding :pagecentre :colormodel 321 :useDingbats :horizontal)) 322 (device (file-name-extension out-file)) 323 (device-info (or (assq (intern (concat ":" device)) 324 org-babel-R-graphics-devices) 325 (assq :png org-babel-R-graphics-devices))) 326 (extra-args (cdr (assq :R-dev-args params))) filearg args) 327 (setq device (nth 1 device-info)) 328 (setq filearg (nth 2 device-info)) 329 (setq args (mapconcat 330 (lambda (pair) 331 (if (member (car pair) allowed-args) 332 (format ",%s=%S" 333 (substring (symbol-name (car pair)) 1) 334 (cdr pair)) "")) 335 params "")) 336 (format "%s(%s=\"%s\"%s%s%s); tryCatch({" 337 device filearg out-file args 338 (if extra-args "," "") (or extra-args "")))) 339 340 (defconst org-babel-R-eoe-indicator "'org_babel_R_eoe'") 341 (defconst org-babel-R-eoe-output "[1] \"org_babel_R_eoe\"") 342 343 (defconst org-babel-R-write-object-command "{ 344 function(object,transfer.file) { 345 object 346 invisible( 347 if ( 348 inherits( 349 try( 350 { 351 tfile<-tempfile() 352 write.table(object, file=tfile, sep=\"\\t\", 353 na=\"\",row.names=%s,col.names=%s, 354 quote=FALSE) 355 file.rename(tfile,transfer.file) 356 }, 357 silent=TRUE), 358 \"try-error\")) 359 { 360 if(!file.exists(transfer.file)) 361 file.create(transfer.file) 362 } 363 ) 364 } 365 }(object=%s,transfer.file=\"%s\")" 366 "Template for an R command to evaluate a block of code and write result to file. 367 368 Has four %s escapes to be filled in: 369 1. Row names, \"TRUE\" or \"FALSE\" 370 2. Column names, \"TRUE\" or \"FALSE\" 371 3. The code to be run (must be an expression, not a statement) 372 4. The name of the file to write to") 373 374 (defun org-babel-R-evaluate 375 (session body result-type result-params column-names-p row-names-p async) 376 "Evaluate R code in BODY." 377 (if session 378 (if async 379 (ob-session-async-org-babel-R-evaluate-session 380 session body result-type column-names-p row-names-p) 381 (org-babel-R-evaluate-session 382 session body result-type result-params column-names-p row-names-p)) 383 (org-babel-R-evaluate-external-process 384 body result-type result-params column-names-p row-names-p))) 385 386 (defun org-babel-R-evaluate-external-process 387 (body result-type result-params column-names-p row-names-p) 388 "Evaluate BODY in external R process. 389 If RESULT-TYPE equals `output' then return standard output as a 390 string. If RESULT-TYPE equals `value' then return the value of the 391 last statement in BODY, as elisp." 392 (cl-case result-type 393 (value 394 (let ((tmp-file (org-babel-temp-file "R-"))) 395 (org-babel-eval org-babel-R-command 396 (format org-babel-R-write-object-command 397 (if row-names-p "TRUE" "FALSE") 398 (if column-names-p 399 (if row-names-p "NA" "TRUE") 400 "FALSE") 401 (format "{function ()\n{\n%s\n}}()" body) 402 (org-babel-process-file-name tmp-file 'noquote))) 403 (org-babel-R-process-value-result 404 (org-babel-result-cond result-params 405 (with-temp-buffer 406 (insert-file-contents tmp-file) 407 (org-babel-chomp (buffer-string) "\n")) 408 (org-babel-import-elisp-from-file tmp-file '(16))) 409 column-names-p))) 410 (output (org-babel-eval org-babel-R-command body)))) 411 412 (defvar ess-eval-visibly-p) 413 414 (defun org-babel-R-evaluate-session 415 (session body result-type result-params column-names-p row-names-p) 416 "Evaluate BODY in SESSION. 417 If RESULT-TYPE equals `output' then return standard output as a 418 string. If RESULT-TYPE equals `value' then return the value of the 419 last statement in BODY, as elisp." 420 (cl-case result-type 421 (value 422 (with-temp-buffer 423 (insert (org-babel-chomp body)) 424 (let ((ess-local-process-name 425 (process-name (get-buffer-process session))) 426 (ess-eval-visibly-p nil)) 427 (ess-eval-buffer nil))) 428 (let ((tmp-file (org-babel-temp-file "R-"))) 429 (org-babel-comint-eval-invisibly-and-wait-for-file 430 session tmp-file 431 (format org-babel-R-write-object-command 432 (if row-names-p "TRUE" "FALSE") 433 (if column-names-p 434 (if row-names-p "NA" "TRUE") 435 "FALSE") 436 ".Last.value" (org-babel-process-file-name tmp-file 'noquote))) 437 (org-babel-R-process-value-result 438 (org-babel-result-cond result-params 439 (with-temp-buffer 440 (insert-file-contents tmp-file) 441 (org-babel-chomp (buffer-string) "\n")) 442 (org-babel-import-elisp-from-file tmp-file '(16))) 443 column-names-p))) 444 (output 445 (mapconcat 446 'org-babel-chomp 447 (butlast 448 (delq nil 449 (mapcar 450 (lambda (line) (when (> (length line) 0) line)) 451 (mapcar 452 (lambda (line) ;; cleanup extra prompts left in output 453 (if (string-match 454 "^\\([>+.]\\([ ][>.+]\\)*[ ]\\)" 455 (car (split-string line "\n"))) 456 (substring line (match-end 1)) 457 line)) 458 (with-current-buffer session 459 (let ((comint-prompt-regexp (concat "^" comint-prompt-regexp))) 460 (org-babel-comint-with-output (session org-babel-R-eoe-output) 461 (insert (mapconcat 'org-babel-chomp 462 (list body org-babel-R-eoe-indicator) 463 "\n")) 464 (inferior-ess-send-input)))))))) "\n")))) 465 466 (defun org-babel-R-process-value-result (result column-names-p) 467 "R-specific processing of return value. 468 Insert hline if column names in output have been requested." 469 (if column-names-p 470 (condition-case nil 471 (cons (car result) (cons 'hline (cdr result))) 472 (error "Could not parse R result")) 473 result)) 474 475 476 ;;; async evaluation 477 478 (defconst ob-session-async-R-indicator "'ob_comint_async_R_%s_%s'") 479 480 (defun ob-session-async-org-babel-R-evaluate-session 481 (session body result-type column-names-p row-names-p) 482 "Asynchronously evaluate BODY in SESSION. 483 Returns a placeholder string for insertion, to later be replaced 484 by `org-babel-comint-async-filter'." 485 (org-babel-comint-async-register 486 session (current-buffer) 487 "^\\(?:[>.+] \\)*\\[1\\] \"ob_comint_async_R_\\(start\\|end\\|file\\)_\\(.+\\)\"$" 488 'org-babel-chomp 489 'ob-session-async-R-value-callback 490 'disable-prompt-filtering) 491 (cl-case result-type 492 (value 493 (let ((tmp-file (org-babel-temp-file "R-"))) 494 (with-temp-buffer 495 (insert 496 (org-babel-chomp body)) 497 (let ((ess-local-process-name 498 (process-name (get-buffer-process session)))) 499 (ess-eval-buffer nil))) 500 (with-temp-buffer 501 (insert 502 (mapconcat 503 'org-babel-chomp 504 (list (format org-babel-R-write-object-command 505 (if row-names-p "TRUE" "FALSE") 506 (if column-names-p 507 (if row-names-p "NA" "TRUE") 508 "FALSE") 509 ".Last.value" 510 (org-babel-process-file-name tmp-file 'noquote)) 511 (format ob-session-async-R-indicator 512 "file" tmp-file)) 513 "\n")) 514 (let ((ess-local-process-name 515 (process-name (get-buffer-process session)))) 516 (ess-eval-buffer nil))) 517 tmp-file)) 518 (output 519 (let ((uuid (org-id-uuid)) 520 (ess-local-process-name 521 (process-name (get-buffer-process session))) 522 (ess-eval-visibly-p nil)) 523 (with-temp-buffer 524 (insert (format ob-session-async-R-indicator 525 "start" uuid)) 526 (insert "\n") 527 (insert body) 528 (insert "\n") 529 (insert (format ob-session-async-R-indicator 530 "end" uuid)) 531 (ess-eval-buffer nil )) 532 uuid)))) 533 534 (defun ob-session-async-R-value-callback (params tmp-file) 535 "Callback for async value results. 536 Assigned locally to `org-babel-comint-async-file-callback' in R 537 comint buffers used for asynchronous Babel evaluation." 538 (let* ((graphics-file (and (member "graphics" (assq :result-params params)) 539 (org-babel-graphical-output-file params))) 540 (colnames-p (unless graphics-file (cdr (assq :colnames params))))) 541 (org-babel-R-process-value-result 542 (org-babel-result-cond (assq :result-params params) 543 (with-temp-buffer 544 (insert-file-contents tmp-file) 545 (org-babel-chomp (buffer-string) "\n")) 546 (org-babel-import-elisp-from-file tmp-file '(16))) 547 (or (equal "yes" colnames-p) 548 (org-babel-pick-name 549 (cdr (assq :colname-names params)) colnames-p))))) 550 551 552 553 ;;; ob-session-async-R.el ends here 554 555 556 (provide 'ob-R) 557 558 ;;; ob-R.el ends here