Imperative Notation

Pascal Serrarens pascalrs@cs.kun.nl
Tue, 16 Feb 1999 15:26:34 +0100


Nick Kallen wrote:
> 
> In terms of brevity, it is quite clear that this last solution is better
> than the explicit environment passing solution. One familiar with all of
> these combinators could write pretty looking code.

> Adding a do notation allows you to program at the level most natural for
> imperatives: do this, do that, etc.

Do expressions are just syntactic suger to a language. In the Haskell 98
report we can find how do expressions can be translated into monadic
programs. From this we can conclude that if we have a good monadic approach
to I/O programming, we can easily obtain a do expression based approach.

Unfortunately, due to the multiple environment passing approach and the
strict support from uniqueness typing, it is rather hard to design a monadic
system for Clean.

Let's take a look at your file_func2 proposal:

> file_func2 = do
>         f1 = openFile "T.icl" FReadText
>         f2 = openFile "T2.icl" FWriteText
>         cs = readline f1
>         writes cs f2
>         close f1
>         close f2
>         return Void

Apart from some difference in the syntax, you propose essentially what is
present in Haskell. This is nice, because we can use their experience.

The main disadvantage of such a scheme is safety. Clean's uniqueness type
system guarantees that environments can be updated safely. Their is no
real need for pointer-like things. However in Haskell (in the monolitic
environment approach as you propose) references are used. This immediately
gives troubles. For example:

- what happes when f1 is used outside file_func2?
    * Haskell uses a complicated rank-2 polymorphism construction to
      avoid this
- what happens when f1 is used after the file has been close?
    * probably some run-time error will occur.
- what happens when f1 is used in two threads within one do expression
    * non-deterministic effects will occur

None of the problems above are present using unique subenvironments, while
most problems are hard to solve for beginner programmers (which you have in
mind).

If we want to avoid these problems, we need to have the subenvironments
explicit. So the most imperative approach looks like (I will use a
variation of your syntax, which -I think- is easier to understand)

file_func2 = do (
    f1       := openFile "T.icl" FReadText
    f2       := openFile "T2.icl" FWriteText
    (cs, f1) := readline f1
    f2       := writes cs f2
    close f1
    close f2
    )

Of course, this is essentially my file_fun2 written down using do expressions.
If we compare this with an normal, non-monadic, Clean approach:

file_func2 e
   #  (f1, e)  = openFile "T.icl" FReadText e
      (f2, e)  = openFile "T1.icl" FWriteText e
      (cs, f1) = readline f1
      f2       = writes cs f2
      e        = close f1 e
      e        = close f2 e
   e

we see that the differences are rather small.

[ you compare your file_fun2 with mine ]

> Now, both your and my code are (essentially) the same number of
> lines/tokens/whatever. But I argue that mine is many fold easier to
> understand and to write. Note how in your file_func2, you're forced to
> explicitly use f1 as a continuation. Knowing when and where to write et, to
> use =\ vs. =\\, ret vs. ret`, what returns two values (e.g., (f1, cs) vs.
> cs)--that's all a huge mental burden, especially as imperatives get long and
> complicated (as they presumably will in the so-called "real world").

Actually I'm not really threading f1 as a value, but rather updating it. For
example, in my approach I could write (with do-notation)

file_func3 = do (
   f1        := openFile "T.icl" FReadText
   (cs, f1`) := readline f1
   (c, f1)   := readc f1
   return (c, cs)

(In this case, the file is closed by the garbage collector)
Now I can be sure that c equals the first character in cs. This is not possible
in your solution. In case you wonder where you need this: in a concurrent
setting you may want to create two thread both consuming an input file in a
different way.

I really like to continue this discussion. To me it seems that do-syntax
(as proposed by you) provides a more imperative feel for functional
programmer, but brings along some typical problems and limitationsclosely
related to imperative programming.

> Pascal, since you seem so willing to devote effort to this issue: as a
> person who works with Clean (I'm assuming your job doesn't entail much IO in
> Clean, but nonetheless), please analyze the statement:
> 
> "The above \"do\" notation would make me a more productive and happier human
> being."

Actually, I/O forms a substantial part of my research at the moment. Right
now I am working on TCP communication, File I/O and message passing in general.
Next to this I developed a concurrent version of the Object I/O library.

My statement is this:

"A haskell-like do notation will simplify I/O programs with the cost that
reasoning about you programs is harder and programs will contain more error which
are more difficult to find."

I really would like to have a better solution as long as safety and flexibility
are preserved.

Pascal.