Macros in Fennel default to not having access to the filesystem by default, which has some security properties that do wonders for static analysis but of course make it impossible to do things like this without an extra flag to disable the sandbox.
For the past few years I’ve been keeping an eye out for examples of legitimate use cases for when you would need a macro to do I/O, and this is the first one I’ve found so far! Eventually we are planning to move towards a declarative macro sandbox that lets you configure it to have read access to a limited set of directories which would allow this, but so far no one’s really complained about disallowing filesystem access altogether.
I wouldn’t say this is too crazy, given the fact that this standard implementation of clojure.repl/source does this!
(defn source-fn
"Returns a string of the source code for the given symbol, if it can
find it. This requires that the symbol resolve to a Var defined in
a namespace for which the .clj is in the classpath. Returns nil if
it can't find the source. For most REPL usage, 'source' is more
convenient.
Example: (source-fn 'filter)"
[x]
(when-let [v (resolve x)]
(when-let [filepath (:file (meta v))]
(when-let [strm (.getResourceAsStream (RT/baseLoader) filepath)]
(with-open [rdr (LineNumberReader. (InputStreamReader. strm))]
(dotimes [_ (dec (:line (meta v)))] (.readLine rdr))
(let [text (StringBuilder.)
pbr (proxy [PushbackReader] [rdr]
(read [] (let [i (proxy-super read)]
(.append text (char i))
i)))
read-opts (if (.endsWith ^String filepath "cljc") {:read-cond :allow} {})]
(if (= :unknown *read-eval*)
(throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown."))
(read read-opts (PushbackReader. pbr)))
(str text)))))))
(defmacro source
"Prints the source code for the given symbol, if it can find it.
This requires that the symbol resolve to a Var defined in a
namespace for which the .clj is in the classpath.
Example: (source filter)"
[n]
`(println (or (source-fn '~n) (str "Source not found"))))
As for example in Elixir that shouldn’t be much harder. However the first approach would work better as there is built in formatter that (most) people use, so you can do Macro.to_string(ast) which will do formatting according to community standards OotB.
This is neat and evil at the same time. I’ve even done something equally horrendous… reading Scheme code and replacing/splicing in other values… in Python. I shudder to even remember it.
At some point I stopped caring about things being “gross”. So much of everything we build on every day is filled with ugly hacks at every layer that it’s almost not worth caring about adding to it. It works, it’s not so gross or brittle that it’s going to break, it’s inefficient at worst. Sometimes I wonder if we, as programmers, view things implicitly that are inefficient as gross despite a solution being otherwise reasonable.
What is somewhat specific is the ability to eval things, thought that’s available in any dynamic language. This is not a small feature: you generally want something like this in any user-extensible software.
The contribution of macros here though is that they give a name to this kind of hackery, which puts it into your active vocabulary!
I’m having a hard time understanding the context here and what’s being achieved.
I see the first table macro, and I see slurp-source, but I don’t see anything actually calling slurp-source. Isn’t that the interesting part?
And I thought the goal of reading the code from the file was to avoid having to pretty print with pr-str? Doesn’t it defeat the purpose if it has to call (pr-str key) anyway?
And on a tangent, as Common Lisp user, I think it’s unfortunate that Clojure has an AST, and also that they decided to use cryptic function names like ->>. Every time I read an article about Clojure it feels like they missed out on the best parts of Lisp.
And on a tangent, as Common Lisp user, I think it’s unfortunate that Clojure has an AST, and also that they decided to use cryptic function names like ->>
You’re right, they should have stuck with simple, sensible names like rplacd.
Clojure didn’t have to consider backwards compatibility, and they had 60 years of programming language research and experience to help think up a better name. I think that’s what bothers me about it - they could have been better, but chose to go backwards (or sideways, at best).
And though CL isn’t perfect, at least rplacd gives a hint about what it’s doing and is easy to search the web for.
And in context, replaca/replacd parallel car/cdr, so it’s not too mysterious what they might do. A symbol name like ->> is so generic I can’t even guess what’s happening.
Clojure didn’t have to consider backwards compatibility, and they had 60 years of programming language research and experience to help think up a better name. I think that’s what bothers me about it
That they did?
And though CL isn’t perfect, at least rplacd gives a hint about what it’s doing and is easy to search the web for.
Yes because “clojure ->>” definitely can’t be searched for. Wait…
And in context, replaca/replacd parallel car/cdr, so it’s not too mysterious what they might do.
So it’s “not too mysterious” if you already how old lisps mangle langage and you rewrite the name to be clearer (because the actual function is rplaca nor replaca).
A symbol name like ->> is so generic I can’t even guess what’s happening.
That would be because it’s both very common and very generic so it gets a generic short recognisable name, across an entire range of threading macros (there’s half a dozen)
It’s a values thing. As in, different communities value different aspects of language design. The Clojure community tends to value terseness (and by extension the friendly community itself which is happy to help overcome that hurdle) over being able to guess what’s happening all the time.
And your example is perhaps a bit unfair. For example some-> and some->> mirror -> and ->>. In context, knowing the latter two, what the former do is at least as apparent as replaca/car. Guessing what car does out of blue isn’t any easier, though. It’s basically already an opaque symbol.
Macros in Fennel default to not having access to the filesystem by default, which has some security properties that do wonders for static analysis but of course make it impossible to do things like this without an extra flag to disable the sandbox.
For the past few years I’ve been keeping an eye out for examples of legitimate use cases for when you would need a macro to do I/O, and this is the first one I’ve found so far! Eventually we are planning to move towards a declarative macro sandbox that lets you configure it to have read access to a limited set of directories which would allow this, but so far no one’s really complained about disallowing filesystem access altogether.
I wouldn’t say this is too crazy, given the fact that this standard implementation of
clojure.repl/sourcedoes this!Small correction:
Should be:
As for example in Elixir that shouldn’t be much harder. However the first approach would work better as there is built in formatter that (most) people use, so you can do
Macro.to_string(ast)which will do formatting according to community standards OotB.This is neat and evil at the same time. I’ve even done something equally horrendous… reading Scheme code and replacing/splicing in other values… in Python. I shudder to even remember it.
At some point I stopped caring about things being “gross”. So much of everything we build on every day is filled with ugly hacks at every layer that it’s almost not worth caring about adding to it. It works, it’s not so gross or brittle that it’s going to break, it’s inefficient at worst. Sometimes I wonder if we, as programmers, view things implicitly that are inefficient as gross despite a solution being otherwise reasonable.
This is not particularly related to Clojure, I do “read your own source code” all the time, last time in Zig here: https://github.com/tigerbeetle/tigerbeetle/blob/090cb9e0858f974fb9a62d98dda3c7d511ffabf3/src/testing/snaptest.zig#L171.
What is somewhat specific is the ability to
evalthings, thought that’s available in any dynamic language. This is not a small feature: you generally want something like this in any user-extensible software.The contribution of macros here though is that they give a name to this kind of hackery, which puts it into your active vocabulary!
I’m having a hard time understanding the context here and what’s being achieved.
I see the first
tablemacro, and I seeslurp-source, but I don’t see anything actually callingslurp-source. Isn’t that the interesting part?And I thought the goal of reading the code from the file was to avoid having to pretty print with
pr-str? Doesn’t it defeat the purpose if it has to call(pr-str key)anyway?And on a tangent, as Common Lisp user, I think it’s unfortunate that Clojure has an AST, and also that they decided to use cryptic function names like
->>. Every time I read an article about Clojure it feels like they missed out on the best parts of Lisp.You’re right, they should have stuck with simple, sensible names like
rplacd.Clojure didn’t have to consider backwards compatibility, and they had 60 years of programming language research and experience to help think up a better name. I think that’s what bothers me about it - they could have been better, but chose to go backwards (or sideways, at best).
And though CL isn’t perfect, at least
rplacdgives a hint about what it’s doing and is easy to search the web for.And in context, replaca/replacd parallel car/cdr, so it’s not too mysterious what they might do. A symbol name like
->>is so generic I can’t even guess what’s happening.That they did?
Yes because “clojure ->>” definitely can’t be searched for. Wait…
So it’s “not too mysterious” if you already how old lisps mangle langage and you rewrite the name to be clearer (because the actual function is rplaca nor replaca).
That would be because it’s both very common and very generic so it gets a generic short recognisable name, across an entire range of threading macros (there’s half a dozen)
It’s a values thing. As in, different communities value different aspects of language design. The Clojure community tends to value terseness (and by extension the friendly community itself which is happy to help overcome that hurdle) over being able to guess what’s happening all the time.
And your example is perhaps a bit unfair. For example
some->andsome->>mirror->and->>. In context, knowing the latter two, what the former do is at least as apparent as replaca/car. Guessing whatcardoes out of blue isn’t any easier, though. It’s basically already an opaque symbol.