Welcome! Please see the About page for a little more info on how this works.

0 votes
ago in Clojure by

I would like to improve Issue 2213 as follows. Do you think, this makes sense? Shall I become contributor for this?

The new description would be:

When developing Input-Process-Output tools I am often in the situation, that I have a sequence of activities (e.g. opening a file, spec checking a file, calling external tools, creating files), which each could fail in certain ways. Clojure does not have a good way to control such a sequence of activities in a way that all sorts of things, which could go wrong, can be handled comfortably. In imperative programming languages concepts like early exit or bailout are used. These are not not very functional. Unfortunately practical problems are often like this.

The issue is a bit more general and a solution would be nice not only for those who develop such sort of tools. So I can give a simple example.

(let [config-file (get-file-from-network xy)
      config-file (parse-edn config-file)
      syntax-ok?   (spec-check config-file)]
  (do-something-with config-file))

As all these steps can fail, we could instead implement it like this.

(if-let [config-file (get-file-from-network xy)]
  (if-let [config-file (parse-edn config-file)]
    (if (spec-check config-file)
      (do-something-with config-file))))

Now the code has suffered already a lot. The sequential nature of the problem is lost. And it is not even doing what we need. We have to report to the user what went wrong.

(if-let [config-file (get-file-from-network xy)]
  (if-let [config-file (parse-edn config-file)]
    (if (spec-check config-file)
      (do-something-with config-file)
      :file-has-syntax-errors)
    :file-not-valid-edn)
  :file-cannot be openned)

But this is still not doing what we need. We have to provide more information about each failure. If a file cannot be openned, is it because the file does not exist or we do not have read permissions? And what did the spec check tell us? So we cannot just use nil as a universal failure. We must deliver more information when failing. And we cannot use if-let, either. So we are not even done.

This problem report asks for a better solution for this conceptual challenge.

This example depicts a conflict that should not happen when using an appropriate programming language: First we are able to quickly make a prototype. But in the end the final code has not much in common with the first code any more. The final design is not driven by the problem domain, but by the technical need for an error handling. The need for a bailout mechanism in this example imposes too many huge code changes.

The principal problem with all these solutions is that they coerce a sequence of actions into something which is not a sequence. If your problem is a sequence, then the implementation should also be a sequence.

Idea: Exceptions

As an alternative implementation I went with Exceptions. But it feels wrong, because I am talking about things, that are expected to fail. I cannot just use the Exceptions that are comming out of Java for example when opening a file for read. I need detailed information, what went wrong. If you are a command line tool, it is expected from you, that you provide good error messages. I ended up catching and rethrowing exceptions everywhere in the program.

Idea: Like the Loop macro

For me I created a small macro, that extends let. Then I use this syntax:

(let [[config-file error] (get-file-from-network xy)
      :escape     error
      config-file (parse-edn config-file)
      syntax-ok   (spec-check config-file)]
  (do-something-with config-file))

This often allows a surprisingly intuitive code. For example here:

(let [[config-file error-code] (get-file-from-network xy)
      :escape     (if (= error-code :does-not-exist) 
                    :file-xy-does-not-exist)
      :escape     (if (= error-code :io-error)
                    :file-xy-cannot-be-read)
      [config-file error-code] (parse-edn config-file)
      :escape     (if error-code :file-xy-not-legal-edn)
      syntax-ok   (spec-check config-file)]
  (do-something-with config-file))

This is inspired by the loop macro. But I don't claim that this is a good solution for Clojure (although I think so). But I think, the let macro is a good candidate when improving how Clojure can be used for such imperative things.

Idea: Using if-Let

I also investigated the solution, that was originally proposed by this issue.

(if-let [config-file (get-file-from-network xy)
         config-file (parse-edn config-file)
         config-file-ok? (spec-check config-file)]
  (do-something-with config-file
  :error))

This problem with this solution is that none of the bindings are available in the else part. That makes it very hard to react accordingly. This is a nice and small new language feature. But I don't think, that in practice it would be of help so often. The example from the original issue was very artificial with mathematical operations that cannot fail. The first draft of this issue with that artificial example does not actally describe a real problem. That is why this issue has completelly been rewritten.

Or course we could still bind everything, which was not failed. But I don't think, we would want that. It would have very interesting consequences to the compiling model.

Idea: Threading macros

As this issue is a about a sequence of activities, a new type of threading macro (e.g. let->) could also be an intuitive solution. But I have not tried anything like this.

Final Words

The principle challenge is, that all these solutions establish a sort of alternative control flow. It is a hard challenge to do this right, especially in a functional language. Like exceptions, that immediatelly exit and magically jump somewhere else. Clojure as a practically language could address this challenge.

Please log in or register to answer this question.

...