5

AFAIK, this is a hallmark feature of LISP dialects: For example, suppose we launch an ongoing LISP program that makes repeated calls to some function named func. We are then able to go inside the lisp REPL, change the definition of func, and then the next time the program makes a call to func, the behavior of the program will be appropriately changed live.

I believe this property is called late binding, although I'm not sure if I'm correct.

The benefit of late binding is that it allows you refactor or even completely change a program while it is still running. This іs analogous in biology to the fact that, as we age, almost every cell in our body gets replaced by a new one over a long enough time horizon (but of course we never notice). It can make systems incredibly flexible.

Question: Is there any way to do something analogously in Haskell? If Haskell doesn't allow this, is there a good reason for it to (i.e., is it making some greater trade-off that makes it all worth it)?

George
  • 6,927
  • 4
  • 34
  • 67
  • That is *not* late binding... Late binding is about *scope*. Consider `function h() {function f() {[this]}; function g() {f();}; return g;}` late binding means that if you do `function f() {[other]}; g = h(); g()` the call to `g` will call the function `f` with `[other]` instead of the one with `[this]`. – Bakuriu Nov 05 '16 at 08:54
  • 2
    @Bakuriu You're thinking of [*dynamic* scoping](https://en.wikipedia.org/wiki/Scope_(computer_science)#Dynamic_scoping) which is sometimes called "late binding". "Late binding" [has other meanings](https://en.wikipedia.org/wiki/Late_binding), for example, it's sometimes used for [dynamic dispatch](https://en.wikipedia.org/wiki/Dynamic_dispatch). It's ultimately a bit of an ambiguous and amorphous term vaguely referring to techniques that delay when decisions need to be made, which includes dynamic scoping. – Derek Elkins left SE Nov 05 '16 at 09:14

2 Answers2

7

A good way to understand declarative programming is to think of it as being timeless. So there is no "the next time the program makes a call to func". If the program behaved different the "next time" in some low-level, operational sense, this would be a violation of purity. Keeping our FP colored lenses firmly in place as we step outside of the program, we note that you can't "change the definition of func", instead what you are doing is making a new program with a different definition of func. So really what you are talking about doing when you "change the definition of func" is abandoning the current program and running a new one. There are Haskell libraries that work just this way such as halive and dyre.

The IO monad does let us model a "before" and "after", so within it, we can talk about "changing" something. Changing "the" program still has the same concerns as above, but we can certainly have a reference cell that holds a bunch of code that we update. Effectively, this is what's happening in (naive implementations of) Lisp. You can actually do this manually, e.g. define an IORef or MVar holding the function you want to change and updating it as appropriate. Now the problem is you usually want to update to brand new functions, so we need to either have a way to describe new functions or to load functions on the fly. The former corresponds to having an interpreter available which nominally is what hint and mueval do (though they actually work more like the following). Doing the latter is dynamic code loading and libraries like plugins, rts-loader, and dynamic-loader do this.

Alternatively, you can take the view (or actually structure it so) that parts of your code are running as a coprocess. At this point, you can use standard IPC mechanisms. "Calling a function" in this context would merely mean sending a message.

Ultimately, none of these provide an experience like a Lisp. The Haskell language itself provides no mechanism or notion of a "place" where a definition is "stored" that could then be updated. Again, conceptually in a Lisp, every definition is stored in a mutable cell and every function call first dereferences that mutable cell to get the current definition. The upshot is every library and technique I've mentioned requires you to plan ahead of time where the change points are or what you want to reload or, at the very least, that you want to reload. There is no notion of "attaching" to a running Haskell process or "breaking" into a debugger. Technically, though, with the technique dyre uses you could relaunch into an executable written in a totally different language, let alone an arbitrarily changed Haskell executable.

Derek Elkins left SE
  • 2,079
  • 12
  • 14
1

To my knowledge haskell cannot support this unless you count wrapping this function in a IO or state monad.

But in a pure program reassigning the definition of func would break referential transparency - which is one of the things that makes haskell such a safe language. Furthermore breaking referential transparency would make lazy evaluation impossible, because evaluating a function - before or after the update would yield two different results and make the program non-deterministic.

Now if you look into GHCi, then you see that you can reassign variables - if you try out

$ > ghci
...
Prelude > let a = (+)
Prelude > let a = (*)

is valid, but this let binding lives in IO I believe - which allows for having side effects like reassigning.

epsilonhalbe
  • 15,637
  • 5
  • 46
  • 74
  • 6
    The repeated let binding thing isn't even really a side effect, it's not limited to (or related to) `IO`, and it's not late binding. Anything *else* you defined that used the name `a` before still refers to `+`. All you've done is declare a *new* variable with a new scope. Since it has the same name as an existing variable in an outer score, it shadows the first definition and makes it impossible to refer to in the new scope. But it hasn't changed the earlier definition in any way. You can do this with `let ... in` in pure non-monadic expressions (or anything else that binds a name). – Ben Nov 05 '16 at 01:02
  • 5
    To see why this is not late binding, try `let a = 1; let x = a; let a = 2; print x`. This will produce the output `1` (as expected from lexical scoping), not `2` (as would be expected if late-binding allowed `a` to be actually modified). (And indeed it has nothing to do with `IO`: `let ... in ...` is an expression that is by no means restricted to having a type with `IO` in it.) – Daniel Wagner Nov 05 '16 at 01:10