gerbil icon indicating copy to clipboard operation
gerbil copied to clipboard

Quasistring.

Open belmarca opened this issue 6 years ago • 14 comments

Please comment on the implementation.

Also make sure to squash, no need for all these commits.

belmarca avatar Oct 30 '19 01:10 belmarca

Docs coming in another commit.

belmarca avatar Oct 30 '19 02:10 belmarca

Summoning @fare for a second opinion.

vyzo avatar Oct 30 '19 09:10 vyzo

Here is one issue that needs to be adressed. The following does not fully work:

> (import :std/misc/text)
> (def t (template/hash "(2 + #{n}) is #{(+ 2 n)}"))
> (pp t)
(lambda (#0=#:ht44)
  (string-append
   "(2 + "
   (std/format#format #1="~a" (hash-get #0# 'n))
   ") is "
   (std/format#format #1# (+ 2 n))
   ""))

The macro does not parse the (+ 2 n) s-exp to properly reference n. Even with a datum->syntax this won't reference the proper variable, as the semantics of template/hash assume our references to be lookups in a hash table.

I will see about having a custom %%ref that, at expansion time, would turn (+ 2 n) into (+ 2 (hash-get ht 'n)).

belmarca avatar Oct 31 '19 01:10 belmarca

We will not be merging templates as their implementation is not considered ready yet.

belmarca avatar Nov 01 '19 14:11 belmarca

I didn't look too deeply into the implementation, but here's what bugs me about the design: you first parse the string as a string, then re-parse it. That's Scala-level lame. The right way to do it is more like what you have in a shell double-quote syntax, or in JavaScript backtick syntax: you parse only once, and can thus easily nest expressions with quasiquoted strings without horrible levels of escaping.

fare avatar Nov 01 '19 16:11 fare

Hi @fare,

Two things:

First, I am not at all surprised that it is suboptimal as this is the first ever parser I wrote. I just brute-forced my way out. I chose to open the string as a port to parse it one character at a time using read-char. The read-all-as-string procedure is used to parse sub-templates. If an expansion-time template variable expands into another template, we have to parse it. This is why I append the remaining quasistring to the newly expanded quasistring, and recurse. I would be glad to learn a thing or two on how to do this more cleanly.

Second, do you have an opinion on the semantics of the quasistring? Does something like

(def (qs name) (quasistring "Hello, #{name}!"))
(displayln (qs "Fare"))

look sound?

belmarca avatar Nov 01 '19 19:11 belmarca

When designing a syntax for that, it pays to think about existing systems that were or weren't successful at it. I believe the shell syntax, and to a lesser point, JavaScript syntax, are appropriate comparison points. Why not leverage shell syntax wholesale, for instance? "Constant part $(function call) ${var} \$escape" Alternatively, port Racket's scribble syntax (if the Racket code doesn't speak to you, try my CL code instead). Recursive syntax is MUCH better than parsing strings.

fare avatar Nov 02 '19 18:11 fare

Thanks @fare, I understand better your position.

What you are proposing is semantically different from what I have here. We could make a distinction between function calls and variables in the DSL. However I chose not to in order to keep the DSL to a strict minimum.

Instead of making the distinction between function calls and variables, I decided to make the distinction between phases. That is, #{} gets evaluated at run-time and ##{} at expansion-time. I don't know how I could get data computed and inserted inside a template at compile time otherwise. For the rest, we just let gerbil (well, %%ref and %%app) make the correct decisions wrt function application and reference.

For example, say I want to have my template reflect how long ago it was compiled. I can do

(import :std/misc/text)

(def (time-since then (now (time->seconds (current-time))))
  (inexact->exact (round (- now then))))

(def (qs name)
  (quasistring*
   "Hello, #{name}
This template was compiled ##{(let (then (time->seconds (current-time)))
(string-append \"#{(time-since \" (number->string then) \")}\"))} seconds ago"))


> (pp qs)
(lambda (#0=#:name56)
  (string-append
   "Hello, "
   (std/format#format #1="~a" #0#)
   "\nThis template was compiled "
   (std/format#format #1# (time-since 1572742630.4156442))
   " seconds ago"))
> (displayln (qs "world"))
Hello, world
This template was compiled 6 seconds ago
> (displayln (qs "world"))
Hello, world
This template was compiled 8 seconds ago
> (displayln (qs "world"))
Hello, world
This template was compiled 10 seconds ago
> (displayln (qs "world"))

which, I agree, has some rather clunky string escaping. This can be fixed three ways: with the use of "sub-quasistrings", user-defined functions/macros or lastly by augmenting the DSL with a way to define variables directly inside the template for further reference.

That last options would require quasistring to return a struct or class in which we could store variables. But that is beyond the scope of what I imagine quasistring to be, for the moment.

I didn't know about Scribble, but it seems to be a much larger DSL, which is precisely what I don't want to implement. I will go through their documentation but as I mentioned, I want quasistring to introduce as little new syntax as possible, letting the user work with gerbil and not a new DSL.

But I understand your point. I am willing to make changes if we can keep the run-time/expansion-time separation explicit. And anything that makes the implementation more maintainable and prettier is welcome.

belmarca avatar Nov 03 '19 01:11 belmarca

I kind of like the handlebars!

vyzo avatar Nov 03 '19 09:11 vyzo

I think quasistring fills a different need than scribble et al. One should be able to build a markup language by using quasistring and user defined macros. This is why I removed the template macros from this PR. Solving that problem is much more involved (and opinionated) and there are probably existing solutions that can be recycled, like Scribble.

belmarca avatar Nov 03 '19 15:11 belmarca

Ping @fare!

belmarca avatar Nov 05 '19 18:11 belmarca

I'm afraid of a 80% solution, when a 100% solution isn't far away: it's an attractive nuisance that only invites people to get into real bad situations.

fare avatar Nov 05 '19 20:11 fare

@fare why do you think it's an 80% solution and what's the 100% solution in that case? This is not intended to be the answer to all templating needs of the world, it is simply string interpolation.

Btw, I think we could reduce this and remove the compile-time evaluation stuff. If you need compile-time eval you can do it with a separate macro that does just that and create a value that is placed in the quasistring.

vyzo avatar Nov 05 '19 20:11 vyzo

I am currently using quasistring by compiling it into a separate package. I don't need it to be in the stdlib for now, we can definitely discuss this more. I also want to know what 20% you find missing.

I can extend this PR to have a quasistring procedure as well as a macro. My implementation is directed by my needs: I currently need compiled templates with nested templates which include both compile-time and run-time data. Hence what you see here.

belmarca avatar Nov 05 '19 21:11 belmarca

Closing. Neat idea but need to reimplement.

belmarca avatar Sep 10 '23 18:09 belmarca