all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: "Clément Pit--Claudel" <clement.pit@gmail.com>
To: Emacs developers <emacs-devel@gnu.org>
Subject: RFC: String interpolation
Date: Wed, 7 Dec 2016 20:13:41 -0500	[thread overview]
Message-ID: <51825111-ace4-f750-4077-026a3b648d27@gmail.com> (raw)


[-- Attachment #1.1.1: Type: text/plain, Size: 2750 bytes --]

Hi all,

Many languages are gaining literal string interpolations features of late (the latest example is Python; see https://www.python.org/dev/peps/pep-0498/).  The TL;DR is that in Python 3.6 one can write this:

    value = 1.3
    print(f'The value is {value + 1:.2f}.')

This prints "The value is 2.30."  Plenty of other languages have similar features.

It would be nice if Emacs offered something similar out of the box. It could be a reader macro, but a plain macro would do just as well, and indeed it's relatively easy to code something up (I attached an example implementation).

With the attached fmt.el, you can write things like this:

    (let ((a 12) (b 15))
      (fmt "$a + $b = $(+ a b)"))
    ⇒ "12 + 15 = 27"

    (let ((password "letmein"))
      (fmt "The password is ${password}"))
    ⇒ "The password is letmein"

    (let ((n 25) (price-per-unit 10.5))
      (fmt "The price for $n units is €$[%.2f](* n price-per-unit)"))
    ⇒ "The price for 25 units is €262.50"

    (fmt "Here's a symbol: '${'a}; "
         "and a padded number: ‘$[%-6.3f]{1}’.")
    ⇒ "Here's a symbol: 'a; and a padded number: ‘1.000 ’."

    (fmt "Welcome to Emacs! "
         "Press $[where-is]{'help-with-tutorial} to open the tutorial.")
    ⇒ "Welcome to Emacs! Press C-h t to open the tutorial."

    (fmt "Today is $[date|%Y-%m-%d](current-time).")
    ⇒ "Today is 2016-12-07."

There's also an fmt-message macro that just calls ‘message’ on the interpolated string. The prototype that I attached supports a range of placeholders:

* $a, $a-b, $abc-123-def expand to the values of the corresponding variables
* ${a}, ${123}, ${[x y z]}, ${(+ 1 2)} expand to the value of the corresponding ELisp forms
* $(+ 1 2), $(buffer-file-name), $(current-column) are a shorthand for ${(…)}
* $[%.2f](+ 1 2) and $[%S]{'(1 2 3)} feed the result of evaluation through format, with the given specifier
* $[whereis]{…}' and $[date|%Y-%m-%d]{…} feed the result of evaluating "…" through a custom formatting function, with an optional format specifier
* $$ produces a literal $ sign

fmt expands the strings at compile time (so they must be constant strings).  For example, (fmt "$a + $b = $[%.2f](+ a b)") more or less expands to

    (concat (fmt--print a) " + " (fmt--print b) " = " (fmt--print (+ a b) 'format "%.2f"))

(fmt--print is a thin wrapper that sets print-depth and print-length).

I'd like some feedback on the following:

* Should this go into MELPA, ELPA, or core?
* What do we think of the syntax? An alternative to $[…]{…} would be ${…}%… (which is a bit trickier to parse, and makes it hard to have custom formatters).

Cheers,
Clément.

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1.1.2: fmt.el --]
[-- Type: text/x-emacs-lisp; name="fmt.el", Size: 5277 bytes --]

;;; fmt.el --- Macro-based string interpolation library  -*- lexical-binding: t; -*-

;; Copyright (C) 2016  Clément Pit-Claudel

;; Author: Clément Pit-Claudel <clement.pitclaudel@live.com>
;; Keywords: convenience, extensions

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:
(defconst fmt--sigil ?$)

;;; Printing utilities

(defun fmt--print (e &optional printer format-sequence)
  "Print E with PRINTER according to optional FORMAT-SEQUENCE.
With no FORMAT-SEQUENCE, use princ.  With no PRINTER, use FORMAT."
  (let ((print-quoted t)
        (print-length nil)
        (print-level nil))
    (if format-sequence
        (funcall (or printer 'format) format-sequence e)
      (prin1-to-string e t))))

;;; Specialized printers

(defun fmt--printers-str (_format-string v)
  "Format V as a string, but return \"\" if V is nil."
  (if v (format "%s" v) ""))

(defun fmt--printers-whereis (_format-string function)
  "Show a keybinding for FUNCTION."
  (substitute-command-keys (concat "\\[" (symbol-name function) "]")))

(defvar fmt--printers-alist '(("str" . fmt--printers-str)
                           ("date" . format-time-string)
                           ("where-is" . fmt--printers-whereis))
  "An alist mapping printer names to formatting functions.
Each formatting function should take a value and a format
sequence, and return a string.")

(defun fmt--get-printer (name)
  "Find printer for NAME."
  (or (cdr (assoc name fmt--printers-alist))
      (error "Unrecognized printer: %S" name)))

;;; Compiling format strings

(defun fmt--read-delimited ()
  "Read a string enclosed in a pair of delimiters."
  (let ((start (point)))
    (forward-list)
    (buffer-substring-no-properties (1+ start) (1- (point)))))

(defun fmt--read-format-sequence ()
  "Read a []-delimited format sequence, such as `[.2f]'."
  (when (eq (char-after) ?\[)
    (let ((str (fmt--read-delimited)))
      (if (string-match-p "^%" str)
          (list ''format str)
        (let* ((parts (split-string str "|")))
          (list (list 'quote (fmt--get-printer (car parts)))
                (mapconcat #'identity (cdr parts) "|")))))))

(defun fmt--read-braces ()
  "Read a {}-delimited form, such as `{12}'."
  (pcase-let* ((start (point))
               (expr (fmt--read-delimited))
               (`(,obj . ,count) (read-from-string expr)))
    (prog1 obj
      (unless (= count (length expr))
        (error "Expecting \"}\", but got %S instead"
               (buffer-substring (+ start count 1) (point-max)))))))

(defun fmt--read-var-name ()
  "Read an alphanumeric+dashes variable name, such as `ab0-cd'."
  (let ((start (point)))
    (if (> (skip-chars-forward "-[:alnum:]") 0)
        (intern (buffer-substring-no-properties start (point)))
      (error "Invalid fmt string when reaching %S"
             (buffer-substring (point) (point-max))))))

(defun fmt--read ()
  "Read one form, a lone sigil, or a variable, plus an optional format.
Returns a list (FORM PRINTER FORMAT-SEQUENCE)."
  (let* ((format-sequence (fmt--read-format-sequence))
         (next-char (char-after)))
    (apply #'list
           (cond
            ((equal next-char ?\() (read (current-buffer)))
            ((equal next-char ?\{) (fmt--read-braces))
            ((equal next-char fmt--sigil) (forward-char) (char-to-string fmt--sigil))
            (t (fmt--read-var-name)))
           format-sequence)))

(defun fmt--compile (expr)
  "Parse and compile a format expression EXPR."
  (with-temp-buffer
    (insert expr)
    (goto-char (point-min))
    (let ((chunks nil)
          (str-start (point))
          (sigil-str (char-to-string fmt--sigil)))
      (while (search-forward sigil-str nil t)
        (let ((str (buffer-substring-no-properties str-start (1- (point)))))
          (push str chunks)
          (push `(fmt--print ,@(fmt--read)) chunks)
          (setq str-start (point))))
      (push (buffer-substring-no-properties str-start (point-max)) chunks)
      (nreverse (seq-remove (lambda (x) (equal x "")) chunks)))))

;;; Public API

(defun fmt--1 (expr)
  "Compile a format expressions EXPR."
  (unless (stringp expr)
    (error "`fmt' requires a constant string"))
  (fmt--compile expr))

(defmacro fmt (&rest exprs)
  "Compile a sequence of format expressions EXPRS."
  (cons 'concat (apply #'append (mapcar #'fmt--1 exprs))))

(defmacro fmt-message (exprs)
  "Display one or more messages EXPRS.
EXPRS is compiled as a format expression."
  `(message "%s" (fmt ,@exprs)))

(provide 'fmt)
;;; fmt.el ends here

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 819 bytes --]

             reply	other threads:[~2016-12-08  1:13 UTC|newest]

Thread overview: 86+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2016-12-08  1:13 Clément Pit--Claudel [this message]
2016-12-08  8:27 ` RFC: String interpolation Andreas Schwab
2016-12-08  8:38   ` Joost Kremers
2016-12-08  9:05     ` Philippe Vaucher
2016-12-08 18:25   ` Clément Pit--Claudel
2016-12-08  9:02 ` Philippe Vaucher
2016-12-08 18:22   ` Clément Pit--Claudel
2016-12-08 14:43 ` Joost Kremers
2016-12-08 18:37   ` Clément Pit--Claudel
2016-12-08 20:04     ` Stefan Monnier
2016-12-09  8:57     ` Joost Kremers
2016-12-08 15:51 ` Drew Adams
2016-12-08 18:21   ` Clément Pit--Claudel
2016-12-08 20:38   ` Richard Stallman
     [not found]   ` <<E1cF5SK-0000HO-EL@fencepost.gnu.org>
2016-12-08 22:04     ` Drew Adams
2016-12-08 19:05 ` Stefan Monnier
2016-12-08 19:31   ` Clément Pit--Claudel
2016-12-08 21:31     ` Stefan Monnier
2016-12-10 16:01       ` Clément Pit--Claudel
2016-12-09 19:19 ` Ted Zlatanov
2016-12-09 22:45   ` Clément Pit--Claudel
2016-12-11  2:53     ` Ted Zlatanov
2016-12-11 18:46       ` Lars Ingebrigtsen
2016-12-11 18:56         ` Clément Pit--Claudel
2016-12-11 19:34           ` Lars Ingebrigtsen
2016-12-11 17:38     ` Stefan Monnier
2016-12-11 17:53       ` Clément Pit--Claudel
2016-12-10 14:11   ` Lars Ingebrigtsen
2016-12-10 14:25     ` Clément Pit--Claudel
2016-12-10 15:39       ` Lars Ingebrigtsen
2016-12-10 15:49         ` Clément Pit--Claudel
2016-12-10 16:01           ` Yuri Khan
2016-12-10 17:58             ` Clément Pit--Claudel
2016-12-11 17:47             ` Stefan Monnier
2016-12-11 19:31               ` Yuri Khan
2016-12-11 20:12                 ` Stefan Monnier
2016-12-11  9:35           ` Richard Stallman
2016-12-11 16:29             ` Clément Pit--Claudel
2016-12-11 18:42             ` Lars Ingebrigtsen
2016-12-12 19:41               ` Richard Stallman
2016-12-11 18:41           ` Lars Ingebrigtsen
2016-12-11 21:05             ` Helmut Eller
2016-12-11 21:26               ` Lars Ingebrigtsen
2016-12-12  2:39                 ` Stefan Monnier
2016-12-12  9:44                   ` Lars Ingebrigtsen
2016-12-12 13:03                     ` Stefan Monnier
2016-12-12 14:47                       ` Lars Ingebrigtsen
2016-12-12 15:04                         ` Stefan Monnier
2016-12-12 15:26                           ` Lars Ingebrigtsen
2016-12-12 17:05                             ` Stefan Monnier
2016-12-12 17:12                             ` Lars Ingebrigtsen
2016-12-12 17:23                               ` Stefan Monnier
2016-12-12 13:40                     ` Clément Pit--Claudel
2016-12-12 14:43                       ` Lars Ingebrigtsen
2016-12-12 14:54                         ` Clément Pit--Claudel
2016-12-12 17:14                           ` Stefan Monnier
2016-12-12 17:46                     ` Paul Eggert
2016-12-12 18:33                       ` Lars Ingebrigtsen
2016-12-12 19:51                       ` Ted Zlatanov
2016-12-12 20:41                         ` Paul Eggert
2016-12-12 23:40                           ` Paul Eggert
2016-12-13 17:42                             ` Richard Stallman
2016-12-13 18:05                               ` Paul Eggert
2016-12-13 23:55                                 ` Lars Ingebrigtsen
2016-12-14  0:07                                   ` Paul Eggert
2016-12-14  0:10                                     ` John Wiegley
2016-12-14  0:14                                       ` Lars Ingebrigtsen
2016-12-14  0:17                                       ` Paul Eggert
2016-12-14  0:26                                         ` John Wiegley
2016-12-14  1:48                                           ` Drew Adams
2016-12-14  2:48                                             ` Elias Mårtenson
2016-12-14  3:33                                               ` Drew Adams
2016-12-14  3:52                                                 ` Elias Mårtenson
2016-12-14 13:52                                             ` Lars Brinkhoff
2016-12-14 13:57                                               ` Lars Brinkhoff
2016-12-14  0:24                                       ` Drew Adams
2016-12-14  0:21                                     ` Clément Pit--Claudel
2016-12-14 14:10                                     ` Lars Ingebrigtsen
2016-12-14 17:00                                 ` Richard Stallman
2016-12-12 19:43                     ` Richard Stallman
2016-12-12 21:11                       ` Lars Ingebrigtsen
2016-12-13 17:41                         ` Richard Stallman
2016-12-13 17:43                           ` Lars Ingebrigtsen
2016-12-14 17:01                             ` Richard Stallman
2016-12-13  1:04                       ` Ted Zlatanov
2016-12-12 16:39                   ` Helmut Eller

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=51825111-ace4-f750-4077-026a3b648d27@gmail.com \
    --to=clement.pit@gmail.com \
    --cc=emacs-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.