From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED!not-for-mail From: =?UTF-8?Q?Cl=c3=a9ment_Pit--Claudel?= Newsgroups: gmane.emacs.devel Subject: RFC: String interpolation Date: Wed, 7 Dec 2016 20:13:41 -0500 Message-ID: <51825111-ace4-f750-4077-026a3b648d27@gmail.com> NNTP-Posting-Host: blaine.gmane.org Mime-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha256; protocol="application/pgp-signature"; boundary="q1GaIicT3uvjNJT1hHBvdBKGwessAKlQ0" X-Trace: blaine.gmane.org 1481159679 12683 195.159.176.226 (8 Dec 2016 01:14:39 GMT) X-Complaints-To: usenet@blaine.gmane.org NNTP-Posting-Date: Thu, 8 Dec 2016 01:14:39 +0000 (UTC) User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Thunderbird/45.5.1 To: Emacs developers Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Thu Dec 08 02:14:35 2016 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([208.118.235.17]) by blaine.gmane.org with esmtp (Exim 4.84_2) (envelope-from ) id 1cEnI7-0001qn-Gw for ged-emacs-devel@m.gmane.org; Thu, 08 Dec 2016 02:14:31 +0100 Original-Received: from localhost ([::1]:43398 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1cEnI9-0000YL-Um for ged-emacs-devel@m.gmane.org; Wed, 07 Dec 2016 20:14:33 -0500 Original-Received: from eggs.gnu.org ([2001:4830:134:3::10]:53596) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1cEnHX-0000Y5-7C for emacs-devel@gnu.org; Wed, 07 Dec 2016 20:13:56 -0500 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1cEnHS-0001Pk-FC for emacs-devel@gnu.org; Wed, 07 Dec 2016 20:13:55 -0500 Original-Received: from mout.kundenserver.de ([212.227.126.131]:64721) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1cEnHS-0001P5-3u for emacs-devel@gnu.org; Wed, 07 Dec 2016 20:13:50 -0500 Original-Received: from [18.189.106.208] ([18.189.106.208]) by mrelayeu.kundenserver.de (mreue004 [212.227.15.168]) with ESMTPSA (Nemesis) id 0Le9A8-1czukK1Kd2-00prqx for ; Thu, 08 Dec 2016 02:13:48 +0100 X-Provags-ID: V03:K0:BgleP7GchdpLreS2iFjswGGEk3CDRGnRM/bjWZTmtQj14bAXi05 A7/t5OWoAk7NsTh9SrrSERsUmKcv9R7vAyg+T0z7oBadsTQ11JZeH/+exifMoYwmugyntbK XbVlG0vOvYyKpYhVNAUHIvjhq4MxaWnOWZniRD/CUYQyc6zStdjTOJa4W4Rhpkv+ydmZZ0J H921wnnFpkCuwUSxX/jdQ== X-UI-Out-Filterresults: notjunk:1;V01:K0:eDJf5yR+Woc=:G9uJY/iTidwF1dj7AebsOh BgLzfslEvJnA0hC7DX4HBr9zqtkFV9MQiLEWh0u5dSopUvLa+qUpX4r6EeX+/4WfWW2GOclSq NhYSP61jhCu2gzNGlenUGcD6FN/Lv2Nm3peSeuvCQDcYj0yfBqTHs3ZYlQBhNM/EtRcPle8mc VQLZ/IiLsuP0/nbF6fEEY/FIP/37LphX7aZex+Aj1SFNWrr6iHl3qkUSJhXvHfQPSIuxZkbH7 tmGgd5BNTC+WB3f9YIPSlkWu3hOa+K5RuK/+1+D8hZ/4WePpfUtk6CCOdiRmXOU/GpcvUZr3u fgcFinaP2nRJYpgLOr8fuF/jx0RZo13hOjo5WoYMjSNMbKT2es/GGEB4UF06yqt24Zc9lyYq2 msRP5b7H0z+zPl9bYck+++2VRGOF8RI+ktTAiC48O8qHk41VGWcdm/cN9gvtnXlGfii8RNBHV f/IOHRgbVZoolreZh/yDXNe3B/A7fvA2qxJ8cvTtD6dgNehu0y+slFwb09PARPESEOto45sM6 jt0QOfmOzTbgg25WDoN0fXDB5CGzjNz8QojH0UJFx3qscVG/x1I6m5e1y3qePK+cgwHnzUno4 jSUgJ1YRwHGp5mQKuxrfXf/TXGnLkp7x7IA0+KCJE1d/Q6h2vD1C7B6mpkVz5Jk9omqZSxnF+ hOGxyLaBok8zhov1Tp9Dqe1LuoNnz5ByStC+fHysnEyMW/sEVt8+6C6j1CL2h+5JUN6w= X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 212.227.126.131 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.org gmane.emacs.devel:210121 Archived-At: This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --q1GaIicT3uvjNJT1hHBvdBKGwessAKlQ0 Content-Type: multipart/mixed; boundary="F7pKnEME68uC1UPRKOCkfkcDW4FGmK8dT"; protected-headers="v1" From: =?UTF-8?Q?Cl=c3=a9ment_Pit--Claudel?= To: Emacs developers Message-ID: <51825111-ace4-f750-4077-026a3b648d27@gmail.com> Subject: RFC: String interpolation --F7pKnEME68uC1UPRKOCkfkcDW4FGmK8dT Content-Type: multipart/mixed; boundary="------------DBC4E2FF1D988FE18E7B97C0" This is a multi-part message in MIME format. --------------DBC4E2FF1D988FE18E7B97C0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable 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-0= 498/). The TL;DR is that in Python 3.6 one can write this: value =3D 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 co= uld be a reader macro, but a plain macro would do just as well, and indee= d it's relatively easy to code something up (I attached an example implem= entation). With the attached fmt.el, you can write things like this: (let ((a 12) (b 15)) (fmt "$a + $b =3D $(+ a b)")) =E2=87=92 "12 + 15 =3D 27" (let ((password "letmein")) (fmt "The password is ${password}")) =E2=87=92 "The password is letmein" (let ((n 25) (price-per-unit 10.5)) (fmt "The price for $n units is =E2=82=AC$[%.2f](* n price-per-unit= )")) =E2=87=92 "The price for 25 units is =E2=82=AC262.50" (fmt "Here's a symbol: '${'a}; " "and a padded number: =E2=80=98$[%-6.3f]{1}=E2=80=99.") =E2=87=92 "Here's a symbol: 'a; and a padded number: =E2=80=981.000 =E2= =80=99." (fmt "Welcome to Emacs! " "Press $[where-is]{'help-with-tutorial} to open the tutorial.") =E2=87=92 "Welcome to Emacs! Press C-h t to open the tutorial." (fmt "Today is $[date|%Y-%m-%d](current-time).") =E2=87=92 "Today is 2016-12-07." There's also an fmt-message macro that just calls =E2=80=98message=E2=80=99= on the interpolated string. The prototype that I attached supports a ran= ge of placeholders: * $a, $a-b, $abc-123-def expand to the values of the corresponding variab= les * ${a}, ${123}, ${[x y z]}, ${(+ 1 2)} expand to the value of the corresp= onding ELisp forms * $(+ 1 2), $(buffer-file-name), $(current-column) are a shorthand for ${= (=E2=80=A6)} * $[%.2f](+ 1 2) and $[%S]{'(1 2 3)} feed the result of evaluation throug= h format, with the given specifier * $[whereis]{=E2=80=A6}' and $[date|%Y-%m-%d]{=E2=80=A6} feed the result = of evaluating "=E2=80=A6" through a custom formatting function, with an o= ptional format specifier * $$ produces a literal $ sign fmt expands the strings at compile time (so they must be constant strings= ). For example, (fmt "$a + $b =3D $[%.2f](+ a b)") more or less expands = to (concat (fmt--print a) " + " (fmt--print b) " =3D " (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 $[=E2=80=A6]{=E2=80=A6= } would be ${=E2=80=A6}%=E2=80=A6 (which is a bit trickier to parse, and = makes it hard to have custom formatters). Cheers, Cl=C3=A9ment. --------------DBC4E2FF1D988FE18E7B97C0 Content-Type: text/x-emacs-lisp; name="fmt.el" Content-Transfer-Encoding: quoted-printable Content-Disposition: attachment; filename="fmt.el" ;;; fmt.el --- Macro-based string interpolation library -*- lexical-bind= ing: t; -*- ;; Copyright (C) 2016 Cl=C3=A9ment Pit-Claudel ;; Author: Cl=C3=A9ment Pit-Claudel ;; 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 . ;;; 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 (=3D 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 --------------DBC4E2FF1D988FE18E7B97C0-- --F7pKnEME68uC1UPRKOCkfkcDW4FGmK8dT-- --q1GaIicT3uvjNJT1hHBvdBKGwessAKlQ0 Content-Type: application/pgp-signature; name="signature.asc" Content-Description: OpenPGP digital signature Content-Disposition: attachment; filename="signature.asc" -----BEGIN PGP SIGNATURE----- Version: GnuPG v2 iQIcBAEBCAAGBQJYSLPFAAoJEPqg+cTm90wjAnAQAJC7rv3JqbL17vmtibGIIsd3 ugdyolatOIECSWXbTVkPbIqWxPyiwwpFBWtAcGrLiSGIWCS1sSmsqIln79Bw8wlo DoeTvZPtlPQyvpnFqNGTLv5yWMx4fOlQ5hzESzRZVtFwCSqk+fDNgoWBckObvDoC gFK6xmIoY23vuPcCuBiJ6dl1PTeItDNLaerE9/Hj2JUiNzFGGQpEAOR54P1qMuee xDicf1RjsXEamS/Cz+XGcDMrlfoKgZX3Mb+tzArWHC6lo4LQwSc1bYheksHUeEq5 MRD8g+IgplgZbfsh8aHUIF50I7vJ5x+U35QrBC1rxD2oAMJ7N5xguV/R3jipBdHC DfxjUwrEtmDfvR7/Ppsxx7/CHhAfZX9WR4DXc/YSwdsstzEDFaKVIe3m5ZiHrbdt ZFSKwMU+Z+8him/XHwLvkY8RJYGd1ECWqcmXYbUhT/xa7kOKCRk6ydFHM5K0WKfS QrynVrTW+cCqAqcJ/iabb7NAm9s/mS1CFMl9OUMEIsvDI3z0qZQAKYr2lrmvt8XR HlEhs9eYu6DtNqUkcSzCpD9/jOiAsaAK6n5E9nX3E7QjEZNOld/V8NOMSqEawz9r aWTUaVRyNpHOEHaR2Rekc1AUcQfvo+EGHgpUSaQEyqKSKChXlkM/XdUdwebCpXE1 YHCS2oVW/9EgBAgYU6tq =HZCH -----END PGP SIGNATURE----- --q1GaIicT3uvjNJT1hHBvdBKGwessAKlQ0--