>> Problem

An early Emacs hack and study is creating a project script loader that when I open a project such as Java or NodeJS, I want to load a custom script specific to that project. Since I am using projectile, it was a great learning experience on understanding symbols and lambdas.

The Emacs way of loading project specific configuration is through =.dir-locals.el= that has an unfortunately elaborate structure to fill out. Emacs is a Lisp interpreter, thus this mechanism enforces safety even in evaluating malicious code since it is possible in a multi-user setup that someone tamper any automatic eval mechanism to cause harm. Remember the saying, eval is evil.

A stronger and direct reason you want to use such mechanism is to avoid tangling your global configuration with a project specific configuration. Keep the business and pure logic separate goes the mantra. My use is with this blog, when I want to write I want to load the org-jekyll-blogger.el setup but not tangle the environment if I'm not.

Although it is the builtin and preferred mechanism, it is slightly frustrating to debug if your script is being run or that the variables are set. Not to mention it is harder to edit. Sometimes you just want to write the code and not worry about security, trust is overrated.

Since the project library projectile does not offer this simple yet security riddled functionality, it leaves the ecosystem to fill in that blank. Packages offering this mechanism already exist such as defproject but it is simple to write the code without relying on a third-party package.


For the impatient, here is the complete snippet; for the curious, let's discuss aspects of it.

>> Project Library

I am using projectile as project managing library. For this task, we need two functions from it:

projectile-project-p : This tells us if the current buffer is in a project.

projectile-project-root : This gives us the current project root the current buffer is in.

projectile-project-name : The optional third function, this prettifies the file path into a more debuggable name.

If you aren't using projectile, Emacs has a builtin project library vc that is tied closely to a VCS. An example of checking if the buffer is in a project.

(defun vcs-project-root ()
  (with-current-buffer (current-buffer)
    (lexical-let ((current-file (buffer-file-name)))
      (or (vc-git-root current-file)
         (vc-svn-root current-file)
         (vc-hg-root current-file)))))

This is rather primitive but it works if the project is under a version control with git, svn or hg. Thankfully, this assumption was not taken by projectile and it finds the root by looking for key files that signify a project such as .git, pom.xml or others. The builtin vc does not cut it; rather, a builtin function that finds the project root is locate-dominating-file. This function takes a file path and file name and recursively travels the parent to find the file name starting at file path. If we assume a project is in a directory that contains a .project.el file, here is the snippet for this:

(defun locate-project-root ()
  (locate-dominating-file (buffer-file-name) ".project.el"))

projectile also has their own copy of this function to avoid depending on files.el where the original comes from. If you have multiple key files aside from .project.el, it is better to create a more performant version of this since you do not want to traverse the disk several times.

The lesson in this is that this job is better left to a library.

>> Core

With this, we implement it quite easily:

(defun fn/load-project-file ()
  "Loads the `fn/project-file' for a project.
This is run once after the project is loaded signifying project setup."
  (when (projectile-project-p) ;; Check if buffer is in a project
    (lexical-let* ((current-project-root (projectile-project-root))
        (project-init-file (expand-file-name ".project.el" current-project-root)))
      (when (file-exists-p project-init-file) ;; Check if project script exists
        (message "Loading project init file for %s" (projectile-project-name)) ;; Some extra logging
        (condition-case ex ;; Load it
            (load project-init-file t)
          ('error ;; Report the error
           (message "There was an error loading %s: %s" project-init-file (error-message-string ex))))))))

(add-hook 'find-file-hook #'fn/load-project-file)

During my early writing, there was a bug that loading the project file would trigger the find-file-hook endlessly. Thankfully, such subtle issue had existed but either way it is simple to write.

>> Symbols Or Lambdas

This quick implementation triggers the project configuration each time a file in the project is opened. What we want is each main project script be loaded once, not every time. This is true for the project locals but not for the main project script.

Thinking functionally, this is memoization of the main loader. We shiv a quick memoization function:

(defun fn/memoize (fn)
  (lexical-let ((fn fn)
      (cache-table (make-hash-table :test 'equal)))
    (lambda (&rest args) ;; Assuming `args' can be used with the hash function `equal'
      (lexical-let ((cached-value
           (gethash args cache-table)))
        (if cached-value
          (lexical-let ((computed-value (apply fn args)))
            (puthash args computed-value cache-table)

(defun fib (n)
  (pcase n
    ((or 1 2) 1)
    (_ (+ (fib (- n 1))
          (fib (- n 2))))))

(lexical-let ((fib-memoized (fn/memoize #'fib)))
   (lambda (n)
     (funcall fib-memoized n))
   (list 3 5 15 30 3 30)))

Not the best implementation but notice we had to use let, funcall and apply to use it instead just the usual function invocation. This syntactic mismatch or hoop is the difference with symbols and lambdas. The memoization function returns a lambda, if we wanted to use it as a function we need to use defun or fset.

(fset 'what-symbol-name (fn/memoize #'fib))

This raises the question what symbol name you should use? Generated or clobbered? Managing symbols is another task but if we just ignore this issue and plugin a lambda for a hook, we get this:

 (lambda () ;; Written hastily, not representative
   (lexical-let ((wrapped-func ;; We are wrapping `fn/load-project-file' since it needs to take an argument
         (lambda (project-file)
     (funcall wrapped-func (buffer-file-name))))

Looks ugly doesn't it, what I find uglier is what is written in find-file-hook:

;; (cl-prettyprint find-file-hook)
((lambda nil
     (defvar --cl-wrapped-func--)
     (let ((--cl-wrapped-func-- (fn/memoize (function
                                             (lambda (project-file)
       (funcall (symbol-value '--cl-wrapped-func--)))))
 #[0 "\302\301!\210\303\304!8\211\207" [buffer-file-name auto-revert-tail-pos make-local-variable 7 file-attributes] 3]

Do you see the lambda standing out from the rest of the symbols? This happens because anonymous functions are represented as a closure object. Here lies the crossroad of being functional in Emacs: symbols overs lambdas.

To express this notion, let me craft a different form for memoization.

>>> Wrapped Symbol

As contrary as this is, it is better to write the wrapped function as another separate function using defun.

(defvar fn/loaded-projects (list))

(defun fn/wrapped-load-project-file ()
  ;; There is a bug here, can you figure it out?
  (lexical-let* ((project-root (projectile-project-root))
      (loaded-project (member project-root fn/loaded-projects)))
    (if loaded-project
      (add-to-list 'fn/loaded-projects project-root))))

(add-hook 'find-file-hook #'fn/wrapped-load-project-file)

So this version looks a little cleaner but exposes an extra internal variable fn/loaded-projects and an excess wrapper for fn/load-project-file. This is contrary in hiding state in the functional style.

Strangely, this is easier to debug and test. If you wanted to test the wrapped function, you set fn/loaded-projects to nil or a value and repeat the test; this is harder to do with a closure. If a bug is in fn/wrapped-load-project-file, you simply reevaluate the function without having to clean or replace the hook value; with a closure, you have a bugged and patched hook coexisting.

I intentionally left a bug in fn/wrapped-load-project-file to demonstrate this. Patch then eval the new version. I don't have to think about the hook management.

(defun fn/wrapped-load-project-file ()
  (when (projectile-project-p) ;; `projectile-project-root' needs a project first
    (lexical-let* ((project-root (projectile-project-root))
        (loaded-project (member project-root fn/loaded-projects)))
      (if loaded-project
        (add-to-list 'fn/loaded-projects project-root)))))

(setq fn/loaded-projects (list)) ;; If you want to reset its state

This correctly loads fn/load-project-file once, what does this tell us anyway?

>>> Symbols Over Function

Am I saying that when I want to memoize a function I need to create an extra variable and wrapper and expose state? Not really. You create a memoizing macro that used defun or fset to alleviate this as with _emacs-memoize_. In being simple, revealing state and using symbols seems to be the way to go.

I had struggled with this at first preferring closures, but it does feel cleaner and simple specially in the context of Emacs. Since everything is extensible, exposing and manipulating state, declaring and advicing private functions, hiding things in Emacs seem to counter the notion of customization.

The notion of encapsulation is not disregarded but rather not preferred. Since (almost) everything id found via describe-function or describe-variable, being open is really the way to go. If you find pain in writing more code for a repeating concept, if abstracting the state and logic is worth it in simplicity and extensibility.

I can't speak for Scheme or Clojure; for me, Emacs has changed my hand and mind in writing lisp.

>> Security

We now shiv a final feature that asks permission or trust in loading the project files. Let us create a secure wrapper for fn/load-project-file:

(defvar fn/loaded-projects (list)
  "Projects that have been loaded by `fn/load-project-file'.")

(defun fn/safe-load-project-file ()
  ;; Similar to `fn/wrapped-load-project-file' but ...
  (when (projectile-project-p)
    (lexical-let ((project-root (projectile-project-root))
        (project-name (projectile-project-name)))
      (when (not (member project-root fn/loaded-projects))
        (if (not (fn/safe-project-p project-root)) ;; ... asks permission first
            (message "Project script for %s is not trusted." project-name)

          (add-to-list 'fn/loaded-projects project-root))))))

What we are left with is implementing the symbol fn/safe-project-p. The question is indeed what scheme? A simple scheme is just to use yes-or-no-p:

(defun fn/safe-project-p (project-root)
   (format "Do you trust the project at %s?" project-root)))

It is as simple as that. Or be more complex and check for last modified time, expiration period, user ownership and what not. For me, I simply ask permission as well but allow for a deeper setup if needed that I am not going to show. I do want to show how to get the last modified time:

(file-attribute-modification-time ;; file-attribute-* and its company

Do explore the other functions such as file-attribute-user-id for getting file attributes.

>> Persistence

If you use this snippet, you might get annoyed when Emacs opens and needs permission when running the projects you allowed previously. What I am talking about is persistence that is a tricky subject in itself. Several tricks exists for this:

Customize Mechanics : But what if you don't use a custom-file?

p-cache : Too rich for my blood

Write the lisp object to file : A bit low level

A simpler builtin mechanism exist: savehist. It primarily works for lispy data and it does save it to a file in savehist-file. I love the ease of use just by adding the variable symbol to savehist-additional-variables. To demonstrate in modifying fn/safe-project-p:

(defun fn/checked-projects (list))

(defun fn/safe-project-p (project-root)
  (lexical-let ((checked-project
        (cdr (assocproject-root fn/checked-projects)) ;; Maybe 'trusted or 'untrusted
    (pcase checked-project
      ('trusted t)
      ('untrusted nil)
      ('unchecked ;; If the project hasn't been trusted yet
       (lexical-let ((trusted
             (format "Do you trust the project at %s?" project-root))))
          (cons project-root (if trusted 'trusted 'untrusted))))))))

(with-eval-after-load 'savehist
  (add-to-list 'savehist-additional-variables 'fn/checked-projects))

The function fn/checked-projects is an association list of the project root string and a symbol of 'trusted or 'untrusted and what we want to persist. As I mentioned, all you have to do is add it to savehist-additional-variables and our preference is persisted without any fuss. Nothing much to say but if you want more details about this simple persistence SaveHist.

>> Conclusion

After all that, the code is still simple to hack without needing to rely on other packages. Getting work done is more important instead of being worried by setup and security but still valid concerns.