4

Given immutability (which is often encouraged and said to be one of building blocks of functional programming) and CQS (which says that commands should not return a value other than void/unit), how do these work together?

For example:

How would you perform a series of commands (eg multiple file copies, which can throw all kinds of exceptions but should not crash the program), and collect their success or failure+reason in a list of results?

I can imagine some solutions in OO like throwing events or giving access to a Report instance (either static or passing it in by reference), but what are the possibilities in functional languages which do not break CQS?

  • 1
    You are basically talking about monads. – Euphoric Aug 17 '15 at 6:29
  • @Euphoric possibly, but as far as I comprehend monads (I don't), doesn't the use of monads require returning them as well, thus breaking cqs? – koenmetsu Aug 17 '15 at 6:31
  • No. You should not judge a command based on it's return value. For example command can return a success indicator. CQS is about separating code that mutates the state from code that doesn't. This is almost perfect mirror in monads, which are used to represent mutations in system. – Euphoric Aug 17 '15 at 6:33
  • 1
    @koenmetsu: I suppose what you would do in Haskell would be to define your commands as IO actions that return a value of type Either, so that you can represent both the error and success result of each action. Then you would sequence these actions and use a writer monad to collect all the result codes. I am not familiar enough with the writer monad to produce an example. Maybe there is some Haskell expert that can confirm if this idea is correct and sketch a solution. – Giorgio Aug 17 '15 at 7:39
8

Given immutability (which is often encouraged and said to be one of building blocks of functional programming) and CQS (which says that commands should not return a value other than void/unit), how do these work together?

There are no commands in functional programming. Period.

A function's result can only depend on its arguments, and it must be the same every time you call the function with the same arguments. There can be no mutation and no side-effects. Functions must be referentially transparent, which means that I can always replace the function application with its result anywhere and everywhere in the program without changing the meaning of the program. (In particular, this means that if I have a procedure that returns void (i.e. nothing), I can replace it with nothing everywhere, which obviously will change the meaning of the program, and thus such a procedure cannot exist in functional programming.)

Of course, something command-like must still be achievable, otherwise your programs would be pretty boring. As Simon Peyton-Jones ones said during an introduction to Haskell, if your program has no side-effects, it cannot print a result, it cannot ask for input, it cannot interact with the network or the file system, all it does is heat up the CPU, after which someone from the audience interjected that heating up the CPU is also a side-effect, so, technically speaking, if you want your program to have no side-effects, you cannot even run it. Clearly, that is ridiculous.

So, side-effects have to be modeled somehow.

One way to do this, is to imagine that you can somehow describe the state of the entire universe in a datastructure and then pass an instance of that datastructure describing the current state of the world into a function, and the function then returns a new instance of that datastructure describing the new state of the world. Of course, this cannot literally work, describing the state of the world requires at least as much memory as the entire world has. It's a conceptual model.

There's a slight problem: the function could pass the old state of the world on to another function, for example, now there are two functions which have the same state of the world, and both of them could return a different new state of the world. Or, a function could hold on to the old state of the world, and later return it back, effectively time-traveling into the past. Of course, this must be somehow prohibited. One solution would be linear types, which are types that ensure that its values are used only once. Clean does this, it has World types, which are a special kind of linear types for such world values.

In Haskell, all side-effects must take place in a monad. Monads are a way of augmenting computation with additional structure. You can imagine it as the monad threading the world value described above through the different functions but without ever actually directly exposing the value to the functions. (In reality, once you have monadic I/O, you don't actually need the world value any more, the monad itself provides the structure for side-effects.) Earlier attempts at modeling I/O in Haskell included I/O based on lazy streams and continuation-based I/O, which also work, but monadic I/O was found to be easiest to work with.

In all these different models, side-effects will always show up in the types of side-effecting functions. IOW, your commands are now functions which return, takes an argument, or both, a value of a side-effecting type.

A nice side-effect (hah!) of this is that different kinds of side-effects can get their own types, instead of having a catch-all "this is a side-effect" type. For example, in Haskell, there is the well-known IO type, which basically says "anything can happen". But there are also types like Reader (reads global state, an OO example would be a global singleton configuration object that is dependency-injected), Writer (writes global state, e.g. a logger, again, typically solved in OO with a global singleton logger object that gets injected), State (mutable shared state, but no other kind of side-effect), STM (Software Transactional Memory, like State but transactional for parallelism).

The "separation" portion of CQS is achieved using the type system. Commands simply have types that are incompatible with queries. I.e. a "command" which changes some global integer will have State Int somewhere in its type, a command which prints an integer will have type Int -> IO Int and so on.

5

CQS is an idiom from object oriented languages that is designed to help avoid the confusion that can be caused when a function unexpectedly mutates state or interacts with the environment.

This is not a problem for a functional language. In functional languages, all such interaction is explicit in the signatures of the functions involved. Hence, CQS serves no purpose in a functional language: commands are by necessity separate due to the fact that they must both consume and return an object representing the part of the environment that they interact with. This object is typically a monad, and such a monad can be used for many purposes, including accumulating results and errors.

  • Thanks for your answer, Jules. "All such interaction is explicit in the signatures" -> am I correctly understanding this as "because its signature will mention eg a monad, Success/Failure,... you know it's a command?" How does that relate to eg reading from a file, which is a Query but can also be represented by Success/Failure or (I think) a monad? Could you explain a bit further? – koenmetsu Aug 18 '15 at 6:06
3

In Haskell, commands are first-class values. Consider the copyFile function from the System.Directory module. Its signature is this:

copyFile :: FilePath -> FilePath -> IO ()

copyFile is a function that takes two FilePaths as its arguments, and returns a command (type IO ()). This command, when executed, copies the file from the first location to the second one. So note that:

  1. The function does not copy the file. It just returns a command.
  2. Evaluating an expression and executing a command are different things in Haskell.
  3. Haskell is so command oriented that it has commands instead of statements or blocks. It is so command-oriented that an executable Haskell program is a a command, written in Haskell, that the compiler translates to object code.

Since commands are first-class values, you can make a list of commands. Suppose you have a list of source/destination pairs:

locations :: [(FilePath, FilePath)]
locations = [("source1", "dest1"), ("source2", "dest2"), ...]

Using map you can turn this into a list of commands that copy the files:

copyCommands :: [IO ()]
copyCommands = map (\(from, to) -> copyFile from to) locations

And Haskell has standard functions for building complex commands out of lists of commands; for example, the sequence_ function that will convert a list of commands into a command that executes the contents in sequence.

copyFiles :: [(FilePath, FilePath)] -> IO ()
copyFiles = sequence_ . map (\(from, to) -> copyFile from to)

Or, written idiomatically:

copyFiles :: [(FilePath, FilePath)] -> IO ()
copyFiles = for_ (uncurry copyFile)

That uses two standard utility functions:

for_ toAction values = sequence_ (map toAction values)

uncurry f (a, b) = f a b

That's not yet what you want, however, because:

  • It doesn't collect the errors that happen during file copies;
  • It will quit copying files when the first one fails.

But that's easy to fix. First we write a wrapper around copyFile that catches its exceptions and wraps the result in an Either type (using tryIO from Control.Error.Util to catch the exceptions, and runExceptT to unwrap it from the monad transformer):

tryCopyFile :: FilePath -> FilePath -> IO (Either IOException ())
tryCopyFile from to = runExceptT (tryIO (copyFile from to))

And now given a list of (from, to) pairs, we can just use the standard traverse function to construct a command that, when executed, executes the individual commands and the list of our their results (failures and successes):

tryCopyFiles :: [(FilePath, FilePath)] -> IO [Either Exception ()]
tryCopyFiles = traverse (uncurry tryCopyFile)

Note the type IO [Either IOException ()]; this is the type of commands (IO ...) that, when executed, produce a list ([...]) whose elements are either an IOException or unit.


So the answer is that, once you get past the very steep learning curve, it's really easy.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.