[comp.lang.scheme] I/O is a special case

E.Ireland@massey.ac.nz (Evan Ireland) (04/30/91)

>It seems to me that the simplest and most "Scheme-like" approach is to add
>	(open-input-file filename [handler])
>	(open-output-file filename [handler])
>	-- a system-dependent code indicating the nature of the problem
>	-- the filename after (system-dependent) expansion
>and if the handler returns normally its value would become the value of
>the call to open-input-stream.  The handler not being supplied, the
>default would be to signal an error.  Someone who -wants- #f to be
>returned has only to do
>	(open-input-file filename (lambda (e n) #f))

I support this proposal.  Let me summarise the good points as I see them.

(1) No changes are required to existing code.  (Chris Hanson points
    out that "a CL-like condition system can be added without changing
    any code, and is totally compatible with the standard.", but this
    approach is simpler).

(2) Novice users, and expert users who *want* their programs to
    terminate with error conditions for these cases, don't have to
    provide "success" continuations.  (Of course a condition system
    would allow this too, if you simply fail to handle the condition).

(3) The idea of optional failure continuations for input/output
    procedures is "easier to understand" than a condition system.

>I prefer the current situation of being able to say that a certain
>procedure accepts arguments of a certain sort and signals an error if
>the arguments are not valid. As Chris said, we can build a condition
>system consistant with this. You are then free to handle the
>conditions as you see fit (e.g. return some special error object).

If you are programming in a mainly functional style, you will probably

(1) Functions, such as sqrt, which are often partial.  It is often
    tidier to check, before applying a function, whether the arguments
    you intend to pass are valid, and the tools for doing this are
    normal function applications e.g.

	(if (< x 0)
            (do-something-with-negative-number)
          (* 5 (sqrt x))

(2) Side-effecting procedures, mostly for I/O.  Here the validity of
    an argument often depends on factors external to the program, e.g.
    the current state of the file system, which cannot be checked by
    the program, unless it resorts to other I/O procedures!

	(if (file-exists? filename)
            (open...)
          (do-something-about-error))

    In this case checking beforehand is not feasible, because the
    state of the file system may change between the file-exists? and
    open... calls.  Also the file-exists? call may fail!
              
>At this juncture, seems to me, we could make the following change to the
>language: for each builtin function, define another version of it with a
>funny-looking name ("%CAR" or whatever) that takes explicit continuation
>arguments for each of its various failure cases.


    (if (null? x)
        (do-somethinhg-with-empty-list)
      (do-something-with (car x)))

For case (2), however, since we cannot check in this way, we need an

--- Error values: these require changes to existing code (where the
    user *wants* termination with an error condition), and if error
    values are (mistakenly) not checked for, they will simply cause
    problems later on, possibly making some problems more difficult to
    trace.

--- Condition systems: would solve the problem, but are more
    cumbersome than simple failure continuations.  Also the error
    handlers are further removed from potential-error-causing
    procedure calls, making things more difficult to follow.

--- Failure continuations: could be added as optional parameters to
    I/O procedures.  Easy to understand, use, and implement, and
    requiring minimal changes to the language.  Note that these are
    "plain" old continuations, not requiring call/cc.

Anyway my view is biased since I am working on I/O systems for purely
functional languages, where a continuation-passing style is adopted
for I/O.  In my scheme, every I/O operation has an error continuation,
but a simple infix operator <> can be used to get the default "error"
continuation, e.g.

    greet str =
       put str "Please enter your name: " <>
       get str <> \ name -> 
       put str ("Hello " ++ name ++ ".\n") <>
       ...

with (a <> b) defined as (a error b); error takes a string as argument
and terminates execution.  Now if I want to add error handling, I

    greet str =
       put str "Please enter your name: " (\ s -> handle_put_error s) $
       get str (\ s -> handle_get_error s) $ \ name -> 
       put str (\ s -> handle_put2_error s) ("Hello " ++ name ++ ".\n") <>
       ...

    -- Note: f $ a = f a	($ is useful for avoiding parentheses)

(This is Haskell syntax).  Now in Scheme, a full continuation-passing
style may not be appropriate for the standard I/O, but optional
failure continuations provide a similar effect to the <> operator
above, with minimal changes required to the language standard.  As I
have noted, error handling for "functions" might be more appropriately
handled by explicit checking.
_______________________________________________________________________________

E.Ireland@massey.ac.nz          Evan Ireland, School of Information Sciences,
  +64 63 69099 x8541          Massey University, Palmerston North, New Zealand.

markf@zurich.ai.mit.edu (Mark Friedman) (05/01/91)

In article <559sis-b@massey.ac.nz> E.Ireland@massey.ac.nz (Evan Ireland) writes:

   >It seems to me that the simplest and most "Scheme-like" approach is to add
   >	(open-input-file filename [handler])
   >	(open-output-file filename [handler])
   >	-- a system-dependent code indicating the nature of the problem
   >	-- the filename after (system-dependent) expansion
   >and if the handler returns normally its value would become the value of
   >the call to open-input-stream.  The handler not being supplied, the
   >default would be to signal an error.  Someone who -wants- #f to be
   >returned has only to do
   >	(open-input-file filename (lambda (e n) #f))

   I support this proposal.  Let me summarise the good points as I see them.

   (1) No changes are required to existing code.  (Chris Hanson points
       out that "a CL-like condition system can be added without changing
       any code, and is totally compatible with the standard.", but this
       approach is simpler).

Passing error continuations and signalling errors is simpler than
just signalling errors?

   (2) Novice users, and expert users who *want* their programs to
       terminate with error conditions for these cases, don't have to
       provide "success" continuations.  (Of course a condition system
       would allow this too, if you simply fail to handle the condition).


   (3) The idea of optional failure continuations for input/output
       procedures is "easier to understand" than a condition system.

We both agree that if you don't want to handle the error, both
approaches are equivalent. So the question is whether

(open-input-file file 
		 (lambda (e n) (my-handler e n)))

is any simpler than

(bind-handler file-open-error my-handler
	      (lambda () (open-input-file file)))

In this, the simplest case, I don't really think that there is any
significant difference. In the most general case you have to have
something like the following for the continuation approach:

(define (my-procedure arg file-input-handler 
		      read-error-handler bad-input-handler)
  (read-and-process (open-input-file (get-file-name arg)
				     file-input-handler)
		    read-error-handler bad-input-handler))

with the list of handlers being passed around getting longer and
longer as your programs get larger and larger. Dynamic error handlers
neatly solve that problem. 

   >I prefer the current situation of being able to say that a certain
   >procedure accepts arguments of a certain sort and signals an error if
   >the arguments are not valid. As Chris said, we can build a condition
   >system consistant with this. You are then free to handle the
   >conditions as you see fit (e.g. return some special error object).

   If you are programming in a mainly functional style, you will probably

   (1) Functions, such as sqrt, which are often partial.  It is often
       tidier to check, before applying a function, whether the arguments
       you intend to pass are valid, and the tools for doing this are
       normal function applications e.g.

	   (if (< x 0)
	       (do-something-with-negative-number)
	     (* 5 (sqrt x))

The question that we are trying to answer is what to do when you don't
(or can't) check whether the arguments are valid.

   (2) Side-effecting procedures, mostly for I/O.  Here the validity of
       an argument often depends on factors external to the program, e.g.
       the current state of the file system, which cannot be checked by
       the program, unless it resorts to other I/O procedures!

   For case (2), however, since we cannot check in this way, we need an

   --- Condition systems: would solve the problem, but are more
       cumbersome than simple failure continuations.  

I still don't see why they are more cumbersome (I argue that they are
less cumbersome). 

       Also the error handlers are further removed from
       potential-error-causing procedure calls, making things more
       difficult to follow.

They are less difficult to follow than procedure calls with extraneous
arguments. In the case where the handler is not bound immediately
before a call to a procedure which might signal the condition
corresponding to that handler, your approach would have to pass the
handler along the call chain ala MY-PROCEDURE above.

   --- Failure continuations: could be added as optional parameters to
       I/O procedures.  Easy to understand, use, and implement, and
       requiring minimal changes to the language.  

Well, I don't agree that they are easier to understand and use.

       Note that these are "plain" old continuations, not requiring
       call/cc.

Actually, they look like "plain" old procedures to me. 

-Mark
--

Mark Friedman
MIT Artificial Intelligence Lab
545 Technology Sq.
Cambridge, Ma. 02139

markf@zurich.ai.mit.edu