Are side-effects possible in pure functional progr

2019-01-22 08:08发布

问题:

I have been trying to wrap my head around functional programming for a while now? I have looked up lambda calculus, LISP, OCML, F# and even combinatorial logic but the main problem I have is how do you do things that require side effects like (interacting with a user, communicating with a remote service, or even handle simulating using random sampling) without violating the fundamental premise of pure functional programming which is, that for a given input the output is deterministic? I hope I am making sense, if not I welcome any attempts to properly educate me. Thanks in advance.

回答1:

Most real-world functional programming is not "pure" in most senses, so half of the answer to your question is "you do it by giving up on purity". That said, there are alternatives.

In the "purest" sense of pure, the entire program represents a single function of one or more arguments, returning a value. If you squint your eyes and wave your hands a bit, you can declare that all user input is part of the function's "arguments" and that all output is part of the "return value" and then fudge things a bit so that it only does the actual I/O "on demand".

A similar perspective is to declare that the input to the function is "the entire state of the outside world" and that evaluating the function returns a new, modified "state of the world". In that case, any function in the program that uses the world state is obviously freed from being "deterministic" since no two evaluations of the program will have exactly the same outside world.

If you wanted to write an interactive program in the pure lambda calculus (or something equivalent, such as the esoteric language Lazy K), that's conceptually how you'd do it.

In more practical terms, the problem comes down to making sure that I/O occurs in the correct order when input is being used as an argument to a function. The general structure of the "pure" solution to this problem is function composition. For instance, say you have three functions that do I/O and you want to call them in a certain order. If you do something like RunThreeFunctions(f1, f2, f3) there's nothing to determine the order they'll be evaluated in. On the other hand, if you let each function take another function as an argument, you can chain them like this: f1( f2( f3())), in which case you know that f3 will be evaluated first because the evaluation of f2 depends on its value. [Edit: See also the comment below about lazy vs. eager evaluation. This is important, because lazy evaluation is actually quite common in very pure contexts; e.g., the standard implementation of recursion in the pure lambda calculus is nonterminating under eager evaluation.]

Again, to write an interactive program in the lambda calculus, this is how you'd probably do it. If you wanted something actually usable for programming in, you'd probably want to combine the function composition part with the conceptual structure of functions taking and returning values representing the state of the world, and create some higher-order abstraction to handling pipelining the "world state" values between I/O functions, ideally also keeping the "world state" contained in order to enforce strict linearity--at which point you've all but reinvented Haskell's IO Monad.

Hopefully that didn't just make you even more confused.



回答2:

Haskell is a pure functional programming language. In Haskell all functions are pure (i.e. they always give the same output for the same inputs). But how do you handle side-effects in Haskell? Well, this problem is beautifully solved through the use of monads.

Taking I/O as an example. In Haskell every function that does I/O returns an IO computation, i.e. a computation in the IO monad. So, for instance, a function that reads an int from the keyboard, instead of returning an int, returns an IO computation that yields an int when it is run:

askForInt :: String -> IO Int

Because it returns an I/O computation instead of an Int, you cannot use this result directly in a sum, for example. In order to access the Int value you need to "unwrap" the computation. The only way to do this is to use the bind function (>>=):

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

Because this also returns an IO computation, you always end up with an I/O computation. This is how Haskell isolates side-effects. The IO monad acts as an abstraction of the state of the real world (in fact under the covers it is usually implemented with a type named RealWorld for the state part).



回答3:

Interacting with a user and communicating with a remote service do require some sort of non-functional part to your software.

Many "functional languages", (like most Lisps) are not purely functional. They still let you do things with side-effects, though side-effecty things are "discouraged" in most contexts.

Haskell is "purely functional" but still lets you do non-functional things via the IO monad. The basic idea is that your purely functional program emits a lazy data structure which is evaluated by a non-functional program (which you don't write, it's part of the environment). One could argue that this data structure itself is an imperative program. So you're sort of doing imperative meta-programming in a functional language.

Ignoring which approach is "better", the goal in both cases is to create a separation between the functional and non-functional parts of your programs, and to limit the size of the non-functional parts as much as possible. The functional parts tend to be more reusable, testable, and easier to reason about.



回答4:

Functional Programming is about limiting & isolating side-effects, not trying to get entirely rid of them... because you can't.

... and yes I find FP useful (certainly with Erlang anyways): I find it is easier to get from "idea" to "program" (or problem to solution ;)... but of course that could just be me.



回答5:

You need to know at least another essential concept: Monads. You will need this to do I/O and the other "useful" stuff!



回答6:

The way Haskell does it is by using monads see wikipedia and the explanation by Haskell on their page.

Basically the idea is that you do not get rid of the IO monad. My understanding is that you are able to chain functions that unwrap an IO monad and execute that function. But you are not able to remove the IO monad altogether.

Another example using monads that is not directly tied to IO is the Maybe Monad. This monad is 'unwrappable' in contrary to the IO monad. But it is easier to explain the use of monads using the Maybe monad. Let's assume you have the following function.

wrap :: Maybe x -> (x -> y) -> Maybe y
wrap Nothing  f = Nothing
wrap (Just x) f = Just (f x)

now you can call wrap (Just 4) (5+) which will return Just 9.

The idea of the IO-monad is that you can use functions like (+5) on the internal type. The monad will assure that the functions will be called in serial, because every function is chained with the wrapping IO-monad.



回答7:

The only completely pure functional language I know of is the template system in C++. Haskell takes second place by making the imperative portions of the program explicit.

In Haskell the program has mutable state, but functions (almost always) don't. You keep like 99% percent of program pure, and only the portion that interacts with the outside world is impure. Therefore when you are testing a function, you know there are no side effects. Pure core, with an impure shell.



回答8:

Given that most programs have some effects on the outside world (writing to files, modifying data in a database...) programs as whole are rarely side-effect free. Outside of academic exercises, there is no point in even trying.

But programs are assembled out of building blocks (subroutine, function, method, call it what you want), and pure functions make for very well-behaved building blocks.

Most functional programming languages do not require functions to be pure, although good functional programmers will try to make as many of their functions pure as is feasible and practical, in order to reap the benefits of referential transparency.

Haskell goes further. Every part of a Haskell Programm is pure (at least in the absence of sins such as "unsafePerformIO"). All functions that you write in Haskell are pure.

Side-effects are introduced through monads. They can be used to introduce a sort of "shopping-list -- shopper"-separation. Essentially your program writes a shopping list (which is just data and can be manipulated in a pure fashion), while the language runtime interprets the shopping list and does the effectful shopping. All your code is pure and friendly to equational reasoning and such, whereas the impure code is provided by the compiler-writers.



回答9:

Even if you don't use it in your work, learning one or more functional programming languages is a great way to learn to think differently and gives you a toolkit of alternative approaches to problems (it can also frustrate you when you can't do something as neat and clean as a functional approach in other languages).

And it made me a better at writing XSL stylesheets.