janet-lang / janet

A dynamic language and bytecode vm
https://janet-lang.org
MIT License
3.5k stars 226 forks source link

Redefining functions at runtime? #125

Closed Ruin0x11 closed 5 years ago

Ruin0x11 commented 5 years ago

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:

(defn print-message []
  (print "hoge"))

(defn print-twice []
  (print-message)
  (print-message))

(print-twice)

It prints hoge twice. But I want to print a different string now, so I rewrite print-message.

(defn print-message []
  (print "piyo"))

Then I send just this definition to the running REPL. But when I call (print-twice) it will still print hoge twice instead. It's probably due to Janet using lexical scoping. I would like the changes in print-message to be reflected in print-twice instead by printing piyo 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.

(setdyn :print-message
  (fn []
      (print "hoge")))

(defn print-twice []
  ((dyn :print-message))
  ((dyn :print-message)))

(print-twice)

(setdyn :print-message
  (fn []
      (print "piyo")))

(print-twice)

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 regular defn instead.

bakpakin commented 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.

Ruin0x11 commented 5 years ago

I see, thanks.

PaulBatchelor commented 5 years ago

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.

@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?

bakpakin commented 5 years ago

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)))
PaulBatchelor commented 5 years ago

@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.

pepe commented 5 years ago

Also, it is an excellent macro example!