Skip to content

Commit

Permalink
📚 Clarify leaking of context via lazy-seqs and side effects (#606)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmonks authored Aug 19, 2021
1 parent a618eb8 commit a3e9e4c
Showing 1 changed file with 44 additions and 0 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,50 @@ user=> (sci/eval-string "(with-out-str (foo))" {:bindings {'foo wrapped-foo}})
"yello!\n"
```

**Important note:** despite what was said [above](#usage), forms evaluated by sci that produce
side effects via bindings (e.g. `println`) **can**, in some cases, "escape" the evaluation
context of `sci/eval-string`. Specifically if those side-effecty functions are invoked outside
`sci/eval-string` due to later realisation of a lazy sequence returned from it. For example,
the following code does not work:

````clojure
(let [sw (java.io.StringWriter.)
result (sci/binding [sci/out sw] (sci/eval-string "(map print (range 10))"))]
(println "Output:" (str sw))
(println "Result:" result))
```

The expected behaviour in this case would be:

```
Output: 0123456789
Result: (nil nil nil nil nil nil nil nil nil nil)
```

But the actual behaviour is:

```
Execution error (ClassCastException) at sci.impl.io/pr-on (io.cljc:44).
class sci.impl.vars.SciUnbound cannot be cast to class java.io.Writer (sci.impl.vars.SciUnbound is in unnamed module of loader clojure.lang.DynamicClassLoader @4c2af006; java.io.Writer is in module java.base of loader 'bootstrap')
```

This happens because by the time the lazy-seq is realised, the binding scope for `sci/out`
is long gone, and as a result the lazy-seq can no longer be realised (due to the delayed calls
to `print`, a side-effecty fn that's dependent on the `sci/binding` scope).

The workaround in this case is to always force realization of lazy-sequences, either inside
the string evaluated by sci (e.g. `(doall (map print (range 10)))`) or immediately outside
the call to `eval-string`, but still within the `binding` scope e.g.

```clojure
(let [sw (java.io.StringWriter.)
result (sci/binding [sci/out sw] (doall (sci/eval-string "(map print (range 10))")))]
(println "Output:" (str sw))
(println "Result:" result))
```

Both of these workarounds produce the expected behaviour described above.

### Futures

Creating threads with `future` and `pmap` is disabled by default, but can be
Expand Down

1 comment on commit a3e9e4c

@pmonks
Copy link
Contributor Author

@pmonks pmonks commented on a3e9e4c Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for merging. FYI it looks like the code fence that was supposed to end at line 326 got messed up somehow - see here. It's not obvious to me what's wrong with it, however.

Please sign in to comment.