3 Main Features of Curry

3.15 Input/Output

As we have seen up to now, a Curry program is a set of datatype and function declarations. Functions associate result values to given input arguments. However, application programs must also interact with the “outside” world, i.e., they must read user input, files etc. Traditional programming languages addresses this problem by procedures with side effects, e.g., a procedure read that returns a user input when it is evaluated. Such procedures are problematic in the context of Curry. Firstly, the evaluation time of a function is difficult to control due to the lazy evaluation strategy (see Section 3.12). Secondly, the meaning of functions with side effects is unclear. For instance, if the function readFirstNum returns the first number in a particular file, the evaluation of the expression “2*readFirstNum” yields different values at different points of time (if the contents of the file changes).

Curry solves this problem with the “monadic I/O” concept much like that seen in the functional language Haskell [21]. In the monadic approach to I/O, a program interacting with the outside world is considered as a sequence of actions that change the state of the outside world. Thus, an interactive program computes actions which are applied to a given state of the world (this application is finally done by the operating system that executes a Curry program). As a consequence, the outside world is not directly accessible but can be only manipulated through actions that change the world. Conceptually, the world is encapsulated in an abstract datatype which provides actions to change the world. The type of such actions is “IO t” which is an abbreviation for

World -> (t,World)

where “World” denotes the type of all states of the outside world. If an action of type “IO t” is applied to a particular world, it yields a value of type t and a new (changed) world.

For instance, getChar of type “IO Char” is an action which reads a character from the standard input whenever it is executed, i.e., applied to a world. Similarly, putChar of type “Char -> IO ()” is an action which takes a character and returns an action which, when applied to a world, puts this character to the standard output (and returns nothing, i.e., the unit type). The important point is that values of type World are not accessible to the programmer — she/he can only create and compose actions on the world.

Actions can only be sequentially composed, i.e., one can built a new action that consists of the sequential evaluation of two other actions. The predefined function

(>>) :: IO a -> IO b -> IO b

takes two actions as input and yields an action as the result. The resulting action consists of performing the first action followed by the second action, where the produced value of the first action is ignored. For instance, the value of the expression “putChar 'a' >> putChar 'b'” is an action which prints “ab” whenever it is executed. Using this composition operator, we can define a function putStrLn (which is actually predefined in the prelude) that takes a string and produces an action to print this string:

putStrLn []     = putChar '\n'
putStrLn (c:cs) = putChar c >> putStrLn cs

If two actions should be composed and the value of the first action should be taken into account before performing the second action, the actions can be also composed by the predefined function

(>>=) :: IO a -> (a -> IO b) -> IO b

where the second argument is a function taking the value produced by the first action as input and performs another action. For instance, the action

getChar >>= putChar

is of type “IO ()” and copies, when executed, a character from standard input to standard output. Actually, this composition operator is the only elementary one since the operator “>>” can be defined in terms of “>>=”:

a1 >> a2  =  a1 >>= \_ -> a2

There is also a primitive “empty” action

return :: a -> IO a

that only returns the argument without changing the world. Thus, one can define an “empty” action which returns nothing (i.e., the unit type) as

done :: IO ()
done = return ()

Using these primitives, we can define more complex interactive programs. For instance, an I/O action that copies all characters from the standard input to the standard output up to the first period can be defined as follows [Browse Program][Download Program]:

echo = getChar >>= \c -> if c=='.' then return ()
                                   else putChar c >> echo

Obviously, such a definition is not well readable. Therefore, Curry provides a special syntax extension for writing sequences of I/O actions, called the do notation. The do notation follows the layout style (see Section 3.13.3), i.e., a sequence of actions is vertically aligned so that

do putChar 'a'
   putChar 'b'

is the same as “putChar 'a' >> putChar 'b'”, and

do c <- getChar
   putChar c

is just another notation for “getChar >>= \c -> putChar c”. Thus, the do notation allows a more traditional style of writing interactive programs. For instance, the function echo defined above can be written in the do notation as follows:

echo = do c <- getChar
          if c=='.'
            then return ()
            else do putChar c

As a further example, we show the definition of the I/O action getLine as defined in the prelude. getLine as an action that reads a line from the standard input and returns it:

getLine :: IO String
getLine = do c <- getChar
             if c=='\n'
                then return []
                else do cs <- getLine
                        return (c:cs)

Curry also provides predefined I/O actions for reading files and accessing other parts of the environment. For instance, “readFile f” is an action which returns the contents of file f and “writeFile f s” is an action writing string s into the file f. This allows us to define a function that copies a file with transforming all letters into uppercase ones in a very concise way (toUpper is defined in the standard character library Char and converts lowercase into uppercase letters) [Browse Program][Download Program]:

convertFile input output = do
  s <- readFile input
  writeFile output (map toUpper s)

The function toUpper, defined in the library “Char”, takes a character. If the character is lower case and alphabetic, then it returns it in upper case, otherwise it returns it unchanged. The operation “map” is defined in the “Prelude” and discussed in detail in Section 4.2.6. The combination “map toUpper” transforms all the characters of a string to upper case.

The monadic approach to input/output has the advantage that there are no “hidden” side effects—any interaction with the outside world can be recognized by the IO type of the function. Thus, functions can be evaluated in any order and the only way to combine I/O actions is a sequential one, as one would expect also in other programming languages. However, there is one subtle point. If a function computes non-deterministically different I/O actions, like in the expression “putStrLn (show coin)” (see Section 3.13.1 for the definition of the non-deterministic function coin; show is a predefined function that converts any value into a string), then it is not clear which of the alternative actions should be applied to the world. Therefore, Curry requires that non-determinism in I/O actions must not occur. For instance, we get a run-time error if we evaluate the above expression:

localvar> putStr (show coin)
ERROR: non-determinism in I/O actions occurred!

One way to ensure the absence of such errors is the encapsulation of all search between I/O operations, e.g., by using set functions.

Exercise 6

Define an I/O action fileLength that reads the name of a file from the user and prints the length of the file, i.e., the number of characters contained in this file. [Browse Answer][Download Answer]