Closed Ruin0x11 closed 5 years ago
The semantics around def and defn are unlikely to change, as they are pretty fundamental to the compiler and the language semantics. Not allowing dynamic re-binding unless explicitly allowed makes Janet bytecode much better and able to run easily without keeping around the entire environment table. I agree that dynamic bindings are a bit heavy here and don't look as nice.
I recommend using var
instead of def or dynamic bindings.
(var print-message
(fn []
(print "hoge")))
(defn print-twice []
(print-message)
(print-message))
(print-twice)
(set print-message
(fn []
(print "piyo")))
(print-twice)
Edit: You could of course wrap this up in a macro to make it nicer, maybe varfn
for the first declaration, and setfn
for the re-definitions, or whatever you want to call them.
I see, thanks.
Edit: You could of course wrap this up in a macro to make it nicer, maybe
varfn
for the first declaration, andsetfn
for the re-definitions, or whatever you want to call them.
@bakpakin this sort of functionality would be useful for me. Would it be possible in Janet to combine the functionality of the hypothetical setfn
and varfn
macros into one macro that somehow knows to use set or var?
So I whipped up a little macro that should more or less do what you want. It takes advantage of the fact that the Janet enviornment is a table that can be manually mutated.
(defmacro varfn
"Create a function that can be rebound."
[name & body]
(with-syms [entry old-entry]
~(let [,old-entry (,dyn ',name)]
(def ,entry (or ,old-entry @{:ref @[nil]}))
(setdyn ',name ,entry)
(def f (fn ,name ,;body))
(put-in ,entry [:ref 0] f)
f)))
EDIT:
Here is an improved version that allows docstrings, metadata, and otherwise makes the signature the same as defn
:
(defmacro varfn
"Create a function that can be rebound."
[name & body]
(def expansion (apply defn name body))
(def fbody (last expansion))
(def modifiers (tuple/slice expansion 2 -2))
(def metadata @{})
(each m modifiers
(cond
(keyword? m) (put metadata m true)
(string? m) (put metadata :doc m)
(error (string "invalid metadata " m))))
(with-syms [entry old-entry f]
~(let [,old-entry (,dyn ',name)]
(def ,entry (or ,old-entry @{:ref @[nil]}))
(setdyn ',name ,entry)
(def ,f ,fbody)
(,put-in ,entry [:ref 0] ,f)
(,merge-into ,entry ',metadata)
,f)))
@bakpakin seems to work. super cool. I wouldn't have been able to figure that out on my own. Thanks for writing this up. Going to be very helpful for some creative coding endeavors of mine.
Also, it is an excellent macro example!
I find myself attracted to Lisp-like languages partially because of the ability to "build up" programs by redefining small parts of a program at runtime. As an example, starting with a program like this:
It prints
hoge
twice. But I want to print a different string now, so I rewriteprint-message
.Then I send just this definition to the running REPL. But when I call
(print-twice)
it will still printhoge
twice instead. It's probably due to Janet using lexical scoping. I would like the changes inprint-message
to be reflected inprint-twice
instead by printingpiyo
twice, without needing to augment the syntax too much or re-eval everything.Is this simply against the design principles of Janet or could it be a future possibility? I was thinking it might be possible by the dynamic variables system. In fact, you can do something of the sort with dynamic variables now, except with somewhat awkward syntax.
Maybe there could be a version of
defn
that binds the function to a dynamic variable instead, and a toggle of some kind that allows the function call syntax to attempt to look up the function as a dynamic variable? (though namespacing may complicate this) Then when you don't need dynamic redefinition anymore you could use regulardefn
instead.