Skip to content

String interpolation #231

@borkdude

Description

@borkdude

Below is an adapted version of strint, from core.incubator.

I changed it to not act different wrt/ single and composite values since that didn't seem necessary at all to me.

We could add this to nbb.string, or maybe babashka.string (and also expose it in bb).

EDIT:

Other languages have ${...} and not having a special case for the composite case might make more sense:
${ (+ 1 2 3) }.

since it's fairly common to want to do this: ${ name }s.

This also aligns with JS:

> `Hello $x`
"Hello $x"
> `Hello ${x}`
"Hello 123"
;;; strint.clj -- String interpolation for Clojure
;; originally proposed/published at http://cemerick.com/2009/12/04/string-interpolation-in-clojure/

;; by Chas Emerick <cemerick@snowtide.com>
;; December 4, 2009

;; Copyright (c) Chas Emerick, 2009. All rights reserved. The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution. By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license. You must not
;; remove this notice, or any other, from this software.

(ns
  ^{:author "Chas Emerick",
     :doc "Compile-time string interpolation for Clojure."}
  strint)

(defn- silent-read
  "Attempts to clojure.core/read a single form from the provided String, returning
  a vector containing the read form and a String containing the unread remainder
  of the provided String. Returns nil if no valid form can be read from the
  head of the String."
  [s]
  (try
    (let [r (-> s java.io.StringReader. java.io.PushbackReader.
                ;; currently needed for bb
                clojure.lang.LineNumberingPushbackReader.)]
      [(read r) (slurp r)])
    (catch Exception e (prn e)))) ; this indicates an invalid form -- the head of s is just string data

(defn- interpolate
  "Yields a seq of Strings and read forms."
  ([s atom?]
    (lazy-seq
      (if-let [[form rest] (silent-read (subs s (if atom? 2 1)))]
        (cons form (interpolate (if atom? (subs rest 1) rest)))
        (cons (subs s 0 2) (interpolate (subs s 2))))))
  ([^String s]
    (if-let [start (->> ["~"]
                     (map #(.indexOf s ^String %))
                     (remove #(== -1 %))
                     sort
                     first)]
      (lazy-seq (cons
                  (subs s 0 start)
                  (interpolate (subs s start) false)))
      [s])))

(defmacro <<
  "Accepts one or more strings; emits a `str` invocation that concatenates
the string data and evaluated expressions contained within that argument.
Evaluation is controlled using ~{} and ~() forms. The former is used for
simple value replacement using clojure.core/str; the latter can be used to
embed the results of arbitrary function invocation into the produced string.

Examples:
  user=> (def v 30.5)
  #'user/v
  user=> (<< \"This trial required ~{v}ml of solution.\")
  \"This trial required 30.5ml of solution.\"
  user=> (<< \"There are ~(int v) days in November.\")
  \"There are 30 days in November.\"
  user=> (def m {:a [1 2 3]})
  #'user/m
  user=> (<< \"The total for your order is $~(->> m :a (apply +)).\")
  \"The total for your order is $6.\"
  user=> (<< \"Just split a long interpolated string up into ~(-> m :a (get 0)), \"
           \"~(-> m :a (get 1)), or even ~(-> m :a (get 2)) separate strings \"
           \"if you don't want a << expression to end up being e.g. ~(* 4 (int v)) \"
           \"columns wide.\")
  \"Just split a long interpolated string up into 1, 2, or even 3 separate strings if you don't want a << expression to end up being e.g. 120 columns wide.\"
  
Note that quotes surrounding string literals within ~() forms must be
escaped."
  [& strings]
  `(str ~@(interpolate (apply str strings))))

;; escape ~
(prn (<< "~\\~"))

(prn
 (let [x 1 y 2]
   (<< "x: ~x, y: ~y, z: ~(+ 1 2 3)")))
;; "x: 1, y: 2, z: 6"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions