Files
ob-elixir/docs/01-emacs-elisp-best-practices.md

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

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

References