11 KiB
Emacs Lisp Best Practices for Package Development
This document covers best practices for writing Emacs Lisp packages, specifically targeting org-babel language implementations.
Table of Contents
- File Structure and Headers
- Naming Conventions
- Variables and Customization
- Function Definitions
- Error Handling
- Documentation
- Dependencies and Loading
- References
File Structure and Headers
Every Emacs Lisp package file should follow a standard structure:
;;; ob-elixir.el --- Org Babel functions for Elixir evaluation -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Your Name
;; Author: Your Name <your.email@example.com>
;; URL: https://github.com/username/ob-elixir
;; Keywords: literate programming, reproducible research, elixir
;; Version: 1.0.0
;; Package-Requires: ((emacs "27.1") (org "9.4"))
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package provides Org Babel support for evaluating Elixir code blocks.
;;
;; Features:
;; - Execute Elixir code in org-mode source blocks
;; - Support for Mix project context
;; - Session support via IEx
;; - Variable passing between code blocks
;;
;; Usage:
;; Add (elixir . t) to `org-babel-load-languages':
;;
;; (org-babel-do-load-languages
;; 'org-babel-load-languages
;; '((elixir . t)))
;;; Code:
(require 'ob)
(require 'ob-ref)
(require 'ob-comint)
(require 'ob-eval)
;; ... your code here ...
(provide 'ob-elixir)
;;; ob-elixir.el ends here
Key Header Elements
| Element | Purpose | Example |
|---|---|---|
lexical-binding: t |
Enable lexical scoping (always use this) | First line comment |
Package-Requires |
Declare dependencies with minimum versions | ((emacs "27.1")) |
Keywords |
Help with package discovery | Standard keyword list |
Version |
Semantic versioning | 1.0.0 |
Naming Conventions
Package Prefix
All symbols (functions, variables, constants) must be prefixed with the package name to avoid namespace collisions:
;; GOOD - properly prefixed
(defvar ob-elixir-command "elixir")
(defun ob-elixir-evaluate (body params) ...)
(defconst ob-elixir-eoe-indicator "---ob-elixir-eoe---")
;; BAD - no prefix, will collide
(defvar elixir-command "elixir")
(defun evaluate-elixir (body) ...)
Naming Patterns for org-babel
Org-babel uses specific naming conventions that must be followed:
;; Required function - note the colon syntax
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with Babel.")
;; Optional session function
(defun org-babel-elixir-initiate-session (&optional session params)
"Create or return an Elixir session buffer.")
;; Variable assignments function
(defun org-babel-variable-assignments:elixir (params)
"Return Elixir code to assign variables.")
;; Default header arguments variable
(defvar org-babel-default-header-args:elixir '()
"Default header arguments for Elixir code blocks.")
;; Prep session function
(defun org-babel-prep-session:elixir (session params)
"Prepare SESSION with PARAMS.")
Private Functions
Use double hyphens for internal/private functions:
;; Public API
(defun ob-elixir-evaluate (body params)
"Evaluate BODY with PARAMS.")
;; Internal helper - note the double hyphen
(defun ob-elixir--format-value (value)
"Internal: Format VALUE for Elixir.")
(defun ob-elixir--clean-output (output)
"Internal: Remove ANSI codes from OUTPUT.")
Variables and Customization
User-Customizable Variables
Use defcustom for variables users should configure:
(defgroup ob-elixir nil
"Org Babel support for Elixir."
:group 'org-babel
:prefix "ob-elixir-")
(defcustom ob-elixir-command "elixir"
"Command to invoke Elixir.
Can be a full path or just the command name if in PATH."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-iex-command "iex"
"Command to invoke IEx for session mode."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-mix-command "mix"
"Command to invoke Mix."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-default-session-timeout 30
"Default timeout in seconds for session operations."
:type 'integer
:group 'ob-elixir)
;; For options with specific choices
(defcustom ob-elixir-output-format 'value
"Default output format for results."
:type '(choice (const :tag "Return value" value)
(const :tag "Standard output" output))
:group 'ob-elixir)
Internal Variables
Use defvar for internal state:
(defvar ob-elixir--session-buffer nil
"Current active session buffer.
This is internal state and should not be modified directly.")
(defvar ob-elixir--pending-output ""
"Accumulated output waiting to be processed.")
Constants
Use defconst for values that never change:
(defconst ob-elixir-eoe-indicator "__ob_elixir_eoe__"
"String used to indicate end of evaluation output.")
(defconst ob-elixir-error-regexp "\\*\\* (\\w+Error)"
"Regexp to match Elixir error messages.")
Function Definitions
Function Documentation
Always include comprehensive docstrings:
(defun ob-elixir-evaluate (body params)
"Evaluate BODY as Elixir code according to PARAMS.
BODY is the source code string to execute.
PARAMS is an alist of header arguments.
Supported header arguments:
:session - Name of IEx session to use, or \"none\"
:results - How to handle results (value, output)
:var - Variables to inject into the code
Returns the result as a string, or nil if execution failed.
Example:
(ob-elixir-evaluate \"1 + 1\" \\='((:results . \"value\")))"
...)
Interactive Commands
Mark user-facing commands as interactive:
(defun ob-elixir-check-installation ()
"Check if Elixir is properly installed and accessible.
Display version information in the minibuffer."
(interactive)
(let ((version (shell-command-to-string
(format "%s --version" ob-elixir-command))))
(message "Elixir: %s" (string-trim version))))
Using cl-lib Properly
Prefer cl-lib functions over deprecated cl package:
(require 'cl-lib)
;; Use cl- prefixed versions
(cl-defun ob-elixir-parse-result (output &key (format 'value))
"Parse OUTPUT according to FORMAT."
...)
(cl-loop for pair in params
when (eq (car pair) :var)
collect (cdr pair))
(cl-destructuring-bind (name . value) variable
...)
Error Handling
Signaling Errors
Use appropriate error functions:
;; For user errors (incorrect usage)
(user-error "No Elixir session found for %s" session-name)
;; For programming errors
(error "Invalid parameter: %S" param)
;; Custom error types
(define-error 'ob-elixir-error "Ob-elixir error")
(define-error 'ob-elixir-execution-error "Elixir execution error" 'ob-elixir-error)
(define-error 'ob-elixir-session-error "Elixir session error" 'ob-elixir-error)
;; Signaling custom errors
(signal 'ob-elixir-execution-error (list "Compilation failed" output))
Handling Errors Gracefully
(defun ob-elixir-safe-evaluate (body params)
"Safely evaluate BODY, handling errors gracefully."
(condition-case err
(ob-elixir-evaluate body params)
(ob-elixir-execution-error
(message "Elixir error: %s" (cadr err))
nil)
(file-error
(user-error "Cannot access Elixir command: %s" (error-message-string err)))
(error
(message "Unexpected error: %s" (error-message-string err))
nil)))
Input Validation
(defun ob-elixir-evaluate (body params)
"Evaluate BODY with PARAMS."
;; Validate inputs early
(unless (stringp body)
(error "BODY must be a string, got %S" (type-of body)))
(unless (listp params)
(error "PARAMS must be an alist"))
(when (string-empty-p body)
(user-error "Cannot evaluate empty code block"))
...)
Documentation
Commentary Section
Write helpful commentary:
;;; Commentary:
;; ob-elixir provides Org Babel support for the Elixir programming language.
;;
;; Installation:
;;
;; 1. Ensure Elixir is installed and in your PATH
;; 2. Add to your init.el:
;;
;; (require 'ob-elixir)
;; (org-babel-do-load-languages
;; 'org-babel-load-languages
;; '((elixir . t)))
;;
;; Usage:
;;
;; In an org file, create a source block:
;;
;; #+BEGIN_SRC elixir
;; IO.puts("Hello from Elixir!")
;; 1 + 1
;; #+END_SRC
;;
;; Press C-c C-c to evaluate.
;;
;; Header Arguments:
;;
;; - :session NAME Use a persistent IEx session
;; - :results value Return the last expression's value
;; - :results output Capture stdout
;; - :mix PROJECT Execute in Mix project context
;;
;; See the README for complete documentation.
Info Manual Integration (Optional)
For larger packages, consider an info manual:
(defun ob-elixir-info ()
"Open the ob-elixir info manual."
(interactive)
(info "(ob-elixir)"))
Dependencies and Loading
Requiring Dependencies
;;; Code:
;; Required dependencies
(require 'ob)
(require 'ob-ref)
(require 'ob-comint)
(require 'ob-eval)
;; Soft dependencies (optional features)
(require 'elixir-mode nil t) ; t = noerror
;; Check for optional dependency
(defvar ob-elixir-has-elixir-mode (featurep 'elixir-mode)
"Non-nil if elixir-mode is available.")
Autoloads
For functions that should be available before the package loads:
;;;###autoload
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with Babel."
...)
;;;###autoload
(eval-after-load 'org
'(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
Provide Statement
Always end with provide:
(provide 'ob-elixir)
;;; ob-elixir.el ends here