Multi-argument version of define-meta #215

Closed
opened 4 years ago by basus · 13 comments
basus commented 4 years ago (Migrated from github.com)

Would it be possible to get a multi-argument version of define-meta? I think this would improve readability for blog-like posts, so that instead of

◊define-meta[title]{Some title}
◊define-meta[author]{John Doe}
◊define-meta[published]{2020-02-10}
◊define-meta[edited]{2020-02-20}
◊define-meta[draft]{false}

we could write


◊define-metas[
#:title "Some title"
#:author "John Doe"
#:published "2020-02-10"
#:edited "2020-02-20"
#:draft #f
]{}

I'm not sure if I'd gotten quite the right syntax for this.

Would it be possible to get a multi-argument version of `define-meta`? I think this would improve readability for blog-like posts, so that instead of ``` ◊define-meta[title]{Some title} ◊define-meta[author]{John Doe} ◊define-meta[published]{2020-02-10} ◊define-meta[edited]{2020-02-20} ◊define-meta[draft]{false} ``` we could write ```racket ◊define-metas[ #:title "Some title" #:author "John Doe" #:published "2020-02-10" #:edited "2020-02-20" #:draft #f ]{} ``` I'm not sure if I'd gotten quite the right syntax for this.
sorawee commented 4 years ago (Migrated from github.com)

So first of all, ◊define-meta[draft]{false} would be equivalent to (define-meta draft "false"), so that doesn't quite work. You can use ◊define-meta[draft #f] instead.

I initially thought that you can write a simple user-defined macro to expand define-metas to a bunch of define-meta, but that doesn't quite work because Pollen searches for define-meta before macro expansion happens (or at least that's what I understand). Perhaps @mbutterick could extend the expander to do a local-expand first? Not sure if it's really worth it though, particularly because it will increase the time to lookup metas.

Another solution is to add a support for define-metas into Pollen directly, as you originally requested.

(Also, ◊foo[...]{} and ◊foo[...] are equivalent, so you can drop the braces too)

So first of all, `◊define-meta[draft]{false}` would be equivalent to `(define-meta draft "false")`, so that doesn't quite work. You can use `◊define-meta[draft #f]` instead. I initially thought that you can write a simple user-defined macro to expand `define-metas` to a bunch of `define-meta`, but that doesn't quite work because Pollen searches for `define-meta` before macro expansion happens (or at least that's what I understand). Perhaps @mbutterick could extend the expander to do a `local-expand` first? Not sure if it's really worth it though, particularly because it will increase the time to lookup metas. Another solution is to add a support for `define-metas` into Pollen directly, as you originally requested. (Also, `◊foo[...]{}` and `◊foo[...]` are equivalent, so you can drop the braces too)
sorawee commented 4 years ago (Migrated from github.com)

@mbutterick I'm just throwing ideas here (which could be terrible). Would it be possible to replace the "search for define-meta" with (begin-for-syntax (hash-set! meta ...)) and use (require (for-template ... meta)) on extraction instead? With this way, define-meta could have some computation and play nice with macros while nothing in phase 0 is computed.

@mbutterick I'm just throwing ideas here (which could be terrible). Would it be possible to replace the "search for `define-meta`" with `(begin-for-syntax (hash-set! meta ...))` and use `(require (for-template ... meta))` on extraction instead? With this way, `define-meta` could have some computation and play nice with macros while nothing in phase 0 is computed.
mbutterick commented 4 years ago (Migrated from github.com)

We could extend the define-meta form to accept input like this:

◊(define-meta
   title "Some title"
   author "John Doe"
   published "2020-02-10"
   edited "2020-02-20"
   draft #f)

Which seems close to what you want?

define-meta could have some computation

The reason metas don’t have computation (and a restricted set of valid values) is to guarantee that they can be extracted quickly. IIRC earlier Pollen users overwhelmingly preferred this approach (on the idea that any value that needs to be computed can live inside the body of the Pollen source and be exported).

We could extend the `define-meta` form to accept input like this: ``` ◊(define-meta title "Some title" author "John Doe" published "2020-02-10" edited "2020-02-20" draft #f) ``` Which seems close to what you want? > `define-meta` could have some computation The reason metas don’t have computation (and a restricted set of valid values) is to guarantee that they can be extracted quickly. IIRC earlier Pollen users overwhelmingly preferred this approach (on the idea that any value that needs to be computed can live inside the body of the Pollen source and be exported).
sorawee commented 4 years ago (Migrated from github.com)

@mbutterick I'm not exactly sure what's going on, but extracting data from define-for-syntax is faster than Pollen's current manual data extraction.

Consider introduction.html.pm in your pollen-tfl. Add a file test.rkt with the following content:

#lang racket/base

(require pollen/core)

(select-from-metas 'title "introduction.html.pm")

Here's the result:

$ rm -rf compiled; time racket test.rkt
"introduction"
racket test.rkt  1.50s user 0.41s system 82% cpu 2.305 total

Now, add the following to introduction.html.pm

◊(provide (for-syntax my-title))
◊(require (for-syntax racket/base))
◊(define-for-syntax my-title (string-append "yet " "another " "introduction"))

And change test.rkt to the following:

#lang racket/base

(require pollen/core)

(require (for-template "introduction.html.pm"))

my-title

Here's the result:

$ rm -rf compiled; time racket test.rkt
"yet another introduction"
racket test.rkt  0.86s user 0.19s system 91% cpu 1.149 total

So with this approach, define-meta can be both more expression (can have computation) and faster.

@mbutterick I'm not exactly sure what's going on, but extracting data from `define-for-syntax` is _faster_ than Pollen's current manual data extraction. Consider `introduction.html.pm` in your `pollen-tfl`. Add a file `test.rkt` with the following content: ``` #lang racket/base (require pollen/core) (select-from-metas 'title "introduction.html.pm") ``` Here's the result: ``` $ rm -rf compiled; time racket test.rkt "introduction" racket test.rkt 1.50s user 0.41s system 82% cpu 2.305 total ``` Now, add the following to `introduction.html.pm` ``` ◊(provide (for-syntax my-title)) ◊(require (for-syntax racket/base)) ◊(define-for-syntax my-title (string-append "yet " "another " "introduction")) ``` And change `test.rkt` to the following: ``` #lang racket/base (require pollen/core) (require (for-template "introduction.html.pm")) my-title ``` Here's the result: ``` $ rm -rf compiled; time racket test.rkt "yet another introduction" racket test.rkt 0.86s user 0.19s system 91% cpu 1.149 total ``` So with this approach, `define-meta` can be both more expression (can have computation) and faster.
otherjoel commented 4 years ago (Migrated from github.com)

We could extend the define-meta form to accept input like this:

I really like this idea!

earlier Pollen users overwhelmingly preferred this approach (on the idea that any value that needs to be computed can live inside the body of the Pollen source and be exported)

~Yes, I still think it makes sense to restrict the metas to simple datums.~ edit: @sorawee got me scratchin my chin

> We could extend the `define-meta` form to accept input like this: I really like this idea! > earlier Pollen users overwhelmingly preferred this approach (on the idea that any value that needs to be computed can live inside the body of the Pollen source and be exported) ~Yes, I still think it makes sense to restrict the metas to simple datums.~ edit: @sorawee got me scratchin my chin
mbutterick commented 4 years ago (Migrated from github.com)

@sorawee That’s not quite a fair fight, since select-from-metas will cache the metas on disk (if they haven’t been already). Try this test, for instance:

#lang racket/base

(require (submod (file "introduction.html.pm") metas))

(hash-ref metas 'title)
@sorawee That’s not quite a fair fight, since `select-from-metas` will cache the metas on disk (if they haven’t been already). Try this test, for instance: ```racket #lang racket/base (require (submod (file "introduction.html.pm") metas)) (hash-ref metas 'title) ```
sorawee commented 4 years ago (Migrated from github.com)

Yep, you are right. It's not faster, but have the same performance.

$ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done)
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
( for i in {1..10}; do; racket test.rkt; done; )  8.12s user 1.81s system 83% cpu 11.843 total

$ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done)
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
( for i in {1..10}; do; racket test.rkt; done; )  8.51s user 1.81s system 90% cpu 11.458 total

$ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done)
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
( for i in {1..10}; do; racket test.rkt; done; )  8.62s user 1.86s system 83% cpu 12.522 total

$ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done)
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
"yet another introduction"
( for i in {1..10}; do; racket test.rkt; done; )  8.51s user 1.78s system 91% cpu 11.302 total
Yep, you are right. It's not faster, but have the same performance. ``` $ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done) "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" ( for i in {1..10}; do; racket test.rkt; done; ) 8.12s user 1.81s system 83% cpu 11.843 total $ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done) "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" ( for i in {1..10}; do; racket test.rkt; done; ) 8.51s user 1.81s system 90% cpu 11.458 total $ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done) "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" ( for i in {1..10}; do; racket test.rkt; done; ) 8.62s user 1.86s system 83% cpu 12.522 total $ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done) "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" "yet another introduction" ( for i in {1..10}; do; racket test.rkt; done; ) 8.51s user 1.78s system 91% cpu 11.302 total ```
sorawee commented 4 years ago (Migrated from github.com)

And with caching, the current extraction wins:

$ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done)
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
"introduction"
( for i in {1..10}; do; racket test.rkt; done; )  5.08s user 1.34s system 95% cpu 6.736 total
And with caching, the current extraction wins: ``` $ rm -rf compiled; time (for i in {1..10}; do racket test.rkt; done) "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" "introduction" ( for i in {1..10}; do; racket test.rkt; done; ) 5.08s user 1.34s system 95% cpu 6.736 total ```
mbutterick commented 4 years ago (Migrated from github.com)

In any case, nothing stops you from (ab)using for-syntax as you propose. The point of define-meta is to have an easy-to-explain method of stashing these values. I would have misgivings about using the for-syntax layer for this by default.

In any case, nothing stops you from (ab)using `for-syntax` as you propose. The point of `define-meta` is to have an easy-to-explain method of stashing these values. I would have misgivings about using the `for-syntax` layer for this by default.
mbutterick commented 4 years ago (Migrated from github.com)

BTW I don’t claim that the define-meta form is a miracle of intuitive operation. The analogy to define, after all, is a little strained, because these things end up as keys and values in a hash table. If I had to do it again I would lean harder into that, and have operations like metas-ref and metas-set! instead of define-meta.

BTW I don’t claim that the `define-meta` form is a miracle of intuitive operation. The analogy to `define`, after all, is a little strained, because these things end up as keys and values in a hash table. If I had to do it again I would lean harder into that, and have operations like `metas-ref` and `metas-set!` instead of `define-meta`.
sorawee commented 4 years ago (Migrated from github.com)

If I had to do it again I would lean harder into that, and have operations like metas-ref and metas-set! instead of define-meta.

I actually don't like that at all because the name metas-set! will mislead users in two ways:

  1. They might expect that there could be any computation in the value part.

  2. They might expect that this will work:

    ◊(cond
       [#t (metas-set! title "introduction")
           (void)]
       [else (metas-set! title "introduction 2")
             (void)])
    

    but it obviously cannot, since metas are extracted syntactically. Note that the current define-meta has the same problem too, but its name makes it more likely for users to use it at the top-level.

    Also note that one sensible grammar that will enforce meta definition at the top-level is:

    #lang pollen
    
    #:<META-KEY-1> <META-VALUE-1>
    ...
    #:<META-KEY-n> <META-VALUE-n>
    
    <REGULAR-POLLEN-DOC>
    

The point of define-meta is to have an easy-to-explain method of stashing these values. I would have misgivings about using the for-syntax layer for this by default.

Not sure if I missed anything, but if we restrict define-meta via define-for-syntax to accept only simple datums, then it should for the most part be functionally equivalent to the current define-meta from users' point of view, no? That is to say, all of these are really just implementation details. From users' perspective, nothing should be significantly changed. One minor difference is that it plays nicer with macro expansion -- that is, it will be now possible to define a macro that expands to define-meta.

> If I had to do it again I would lean harder into that, and have operations like `metas-ref` and `metas-set!` instead of `define-meta`. I actually don't like that at all because the name `metas-set!` will mislead users in two ways: 1. They might expect that there could be any computation in the value part. 2. They might expect that this will work: ``` ◊(cond [#t (metas-set! title "introduction") (void)] [else (metas-set! title "introduction 2") (void)]) ``` but it obviously cannot, since metas are extracted syntactically. Note that the current `define-meta` has the same problem too, but its name makes it more likely for users to use it at the top-level. Also note that one sensible grammar that will enforce meta definition at the top-level is: ``` #lang pollen #:<META-KEY-1> <META-VALUE-1> ... #:<META-KEY-n> <META-VALUE-n> <REGULAR-POLLEN-DOC> ``` > The point of define-meta is to have an easy-to-explain method of stashing these values. I would have misgivings about using the `for-syntax` layer for this by default. Not sure if I missed anything, but if we restrict `define-meta` via `define-for-syntax` to accept only simple datums, then it should for the most part be functionally equivalent to the current `define-meta` from users' point of view, no? That is to say, all of these are really just implementation details. From users' perspective, nothing should be significantly changed. One minor difference is that it plays nicer with macro expansion -- that is, it will be now possible to define a macro that expands to `define-meta`.
mbutterick commented 4 years ago (Migrated from github.com)

I actually don't like that at all

Well, then good thing I did it wrong 😉

one sensible grammar that will enforce meta definition

I’m using that pattern in quadwriter to set top-level attributes. I don’t like it for Pollen metas, however, because it’s not prefixed with the command character, so it’s inconsistent with other Pollen commands. I imagine it would also be harder to pluck them out of the source reliably (e.g., it’s easy to pull out metas because they’re always introduced with define-meta)

we restrict define-meta via define-for-syntax

I don’t think I yet understand the benefit of moving metas into the syntax layer, though let’s start a separate issue about that, if you want to make the case.

In the meantime I can make the change requested by @basus.

> I actually don't like that at all Well, then good thing I did it wrong 😉 > one sensible grammar that will enforce meta definition I’m using that pattern in `quadwriter` to set top-level attributes. I don’t like it for Pollen metas, however, because it’s not prefixed with the `◊` command character, so it’s inconsistent with other Pollen commands. I imagine it would also be harder to pluck them out of the source reliably (e.g., it’s easy to pull out metas because they’re always introduced with `define-meta`) > we restrict define-meta via define-for-syntax I don’t think I yet understand the benefit of moving metas into the syntax layer, though let’s start a separate issue about that, if you want to make the case. In the meantime I can make the change requested by @basus.
basus commented 4 years ago (Migrated from github.com)

Thanks for making the change! I didn't follow all the discussion above, and I'm not familiar enough with Racket to be able to understand the issues with define-for-syntax, but this should be helpful for now.

One potential use case: you want to have a meta like ◊(define-meta "last-edited" 'today), and then 'today gets replaced with the actual current date (or the last-modified date on the file). But I think this can (and should) be easily handled by code outside the meta definition.

Thanks for making the change! I didn't follow all the discussion above, and I'm not familiar enough with Racket to be able to understand the issues with `define-for-syntax`, but this should be helpful for now. One potential use case: you want to have a meta like `◊(define-meta "last-edited" 'today)`, and then `'today` gets replaced with the actual current date (or the last-modified date on the file). But I think this can (and should) be easily handled by code outside the meta definition.
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: mbutterick/pollen#215
Loading…
There is no content yet.