Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell with emacs org-mode: Variable not in scope

After wandering off in frustration from before, I've decided to try Haskell in Emacs org-mode again. I'm using Haskell stack-ghci (8.6.3), Emacs 26.2, org-mode 9.2.3 set up with intero. This code block

#+begin_src haskell :results raw :session *haskell*
pyth2 :: Int -> [(Int, Int, Int)]
pyth2 n =
  [ (x, y, z)
  | x <- [1 .. n]
  , y <- [x .. n]
  , z <- [y .. n]
  , x ^ 2 + y ^ 2 == z ^ 2
  ]
#+end_src

produces this RESULTS:

*Main| *Main| *Main| *Main| *Main| 
<interactive>:59:16: error: Variable not in scope: n
<interactive>:60:16: error: Variable not in scope: n
<interactive>:61:16: error: Variable not in scope: n

However, this

#+begin_src haskell :results raw
tripleMe x = x + x + x
#+end_src

works fine. I've added the :set +m to both ghci.conf and the individual code block to no effect. This code works fine in a separate hs file run in a separate REPL. The pyth2 code in a separate file also can be called from the org-mode started REPL and run just fine as well. Not sure how to proceed. Can include Emacs init info if necessary.

like image 905
147pm Avatar asked Dec 23 '22 23:12

147pm


2 Answers

Over on the org-mode mailing list I got an answer that basically is saying the same as you, D. Gillis. He had a similar work-around that actually is more org-mode-centric. Under a heading where your code blocks will be put this "drawer"

:PROPERTIES:
:header-args:haskell: :prologue ":{\n" :epilogue ":}\n"
:END:

and then (possibly in a local variable) run

#+begin_src haskell :results output
:set prompt-cont ""
#+end_src

For reasons unknown I've had to include the :results output otherwise a cryptic error of "expecting a string" happens.

On a few other notes, haskell babel doesn't respond/care about the :session option, i.e., when you run a code block, a REPL *haskell* starts and that will be the sole REPL. Also, a haskell-mode started REPL doesn't play well with an existing org-mode initiated REPL, i.e., if you start a REPL from haskell-mode, it kills the original org-mode *haskkell*REPL, and any new attempt to run org-mode code blocks can't see this new, non-*haskell*REPL. Then if you kill the haskell-mode REPL and try to run org-mode blocks, you get

executing Haskell code block...
inferior-haskell-start-process: List contains a loop: ("--no-build" "--no-load" "--ghci-options=-ferror-spans" "--no-build" "--no-load" . #2)

... you're hosed -- and nothing seems to shake it, not any restart/refresh, nor killing, reloading the file, i.e., a complete restart of Emacs is necessary. Anyone knowing a better solution, please tells usses.

like image 66
147pm Avatar answered Dec 27 '22 09:12

147pm


This is a GHCi issue.

The same error occurs when your code is copied directly into GHCi, which also gives a parse error when it encounters the new line after the equal sign. This first error isn't showing up here because org-babel only shows the value of the last expression (in this case, the error caused by the list comprehension).

I'm not entirely familiar with how Haskell-mode sends the code to GHCi, but it looks like it involves loading in the buffer into GHCi as a file, which may be why you didn't have this problem working from the hs file.

There are a few options to fix this, none of which are completely ideal:

  1. Move some portion of the list into the first line (e.g. the first line could be pyth2 n = [).
  2. Wrap the entire function definition with :{ and :}.
  3. Write an Elisp function to modify what is being sent to GHCi and then changes it back after it is evaluated.

The first two options require you to format your code in a form that the GHCi will accept. In your example case, the first option may not be too bad, but this won't always be so trivial for all multi-line declarations (e.g. pattern-matching function declarations). The downside to the second option is that it requires adding brackets to the code that shouldn't be there in real source code.

To fix the issue of extraneous brackets being added, I've written an Elisp command (my-org-babel-execute-haskell-blocks) that places these brackets around code blocks that it finds, evaluates the region, and then deletes the brackets. Note that this function requires that blocks be separated from all other code with at least one empty line.

Calling my-org-babel-execute-haskell-blocks on your example declares the function without any errors.

EDIT: The previous function I gave failed to work on pattern matching declarations. I've rewritten the function to fix this issue as well as to be comment aware. This new function should be significantly more useful. However, it's worth noting that I didn't handle multi-line comments in a sophisticated manner, so code blocks with multi-line comments may not be wrapped properly.

(defun my-org-babel-execute-haskell-blocks ()
  "Wraps :{ and :} around all multi-line blocks and then evaluates the source block.
Multi-line blocks are those where all non-indented, non-comment lines are declarations using the same token."
  (interactive)
  (save-excursion
    ;; jump to top of source block
    (my-org-jump-to-top-of-block)
    (forward-line)
    ;; get valid blocks
    (let ((valid-block-start-ends (seq-filter #'my-haskell-block-valid-p (my-get-babel-blocks))))
      (mapcar #'my-insert-haskell-braces valid-block-start-ends)
      (org-babel-execute-src-block)
      (mapcar #'my-delete-inserted-haskell-braces (reverse valid-block-start-ends)))))


(defun my-get-blocks-until (until-string)
  (let ((block-start nil)
        (block-list nil))
    (while (not (looking-at until-string))
      (if (looking-at "[[:space:]]*\n")
          (when (not (null block-start))
            (setq block-list (cons (cons block-start (- (point) 1))
                                   block-list)
                  block-start nil))
        (when (null block-start)
          (setq block-start (point))))
      (forward-line))
    (when (not (null block-start))
      (setq block-list (cons (cons block-start (- (point) 1))
                             block-list)))))

(defun my-get-babel-blocks ()
  (my-get-blocks-until "#\\+end_src"))

(defun my-org-jump-to-top-of-block ()
  (forward-line)
  (org-previous-block 1))

(defun my-empty-line-p ()
  (beginning-of-line)
  (= (char-after) 10))

(defun my-haskell-type-declaration-line-p ()
  (beginning-of-line)
  (and (not (looking-at "--"))
       (looking-at "^.*::.*$")))

(defun my-insert-haskell-braces (block-start-end)
  (let ((block-start (car block-start-end))
        (block-end (cdr block-start-end)))
    (goto-char block-end)
    (insert "\n:}")
    (goto-char block-start)
    (insert ":{\n")))


(defun my-delete-inserted-haskell-braces (block-start-end)
  (let ((block-start (car block-start-end))
        (block-end (cdr block-start-end)))
    (goto-char block-start)
    (delete-char 3)
    (goto-char block-end)
    (delete-char 3)))


(defun my-get-first-haskell-token ()
  "Gets all consecutive non-whitespace text until first whitespace"
  (save-excursion
    (beginning-of-line)
    (let ((starting-point (point)))
      (re-search-forward ".*?[[:blank:]\n]")
      (goto-char (- (point) 1))
      (buffer-substring-no-properties starting-point (point)))))


(defun my-haskell-declaration-line-p ()
  (beginning-of-line)
  (or (looking-at "^.*=.*$")  ;; has equals sign
      (looking-at "^.*\n[[:blank:]]*|")
      (looking-at "^.*where[[:blank:]]*$")))


(defun my-haskell-block-valid-p (block-start-end)
  (let ((block-start (car block-start-end))
        (block-end (cdr block-start-end))
        (line-count 0))
        (save-excursion
          (goto-char block-start)
          (let ((token 'nil)
                (is-valid t))
            ;; eat top comments
            (while (or (looking-at "--")
                       (looking-at "{-"))
              (forward-line))
            (when (my-haskell-type-declaration-line-p)
              (progn
                (setq token (my-get-first-haskell-token)
                      line-count 1)
                (forward-line)))
            (while (<= (point) block-end)
              (let ((current-token (my-get-first-haskell-token)))
                (cond ((string= current-token "") ; line with indentation
                       (when (null token) (setq is-valid nil))
                       (setq line-count (+ 1 line-count)))
                      ((or (string= (substring current-token 0 2) "--") ;; skip comments
                           (string= (substring current-token 0 2) "{-"))
                       '())
                      ((and (my-haskell-declaration-line-p)
                            (or (null token) (string= token current-token)))
                       (setq token current-token
                             line-count (+ 1 line-count)))
                      (t (setq is-valid nil)
                         (goto-char (+ 1 block-end))))
                (forward-line)))
            (and is-valid (> line-count 1))))))
like image 45
D. Gillis Avatar answered Dec 27 '22 10:12

D. Gillis