'How to perform side-effects in pure functional programming?

I am dealing with the concept of functional programming for a while now and find it quite interesting, fascinating and exciting. Especially the idea of pure functions is awesome, in various terms.

But there is one thing I do not get: How to deal with side-effects when restricting yourself to pure functions.

E.g., if I want to calculate the sum of two numbers, I can write a pure function (in JavaScript):

var add = function (first, second) {
  return first + second;
};

No problem at all. But what if I want to print the result to the console? The task of "printing something to the console" is not pure by definition - but how could / should I deal with this in a pure functional programming language?



Solution 1:[1]

Haskell, a pure functional language, handles "impure" functions using "monads." A monad is basically a pattern that makes it easy to chain function calls with continuation passing. Conceptually, the print function in Haskell basically takes three parameters: the string to be printed, the state of the program, and the rest of the program. It calls the rest of the program while passing in a new state of the program where the string is on the screen. This way no state has been modified.

There are many in-depth explanations of how monads work because for some reason people think it's a concept that's difficult to grasp: it's not. You can find many by searching on the Internet, I think this is one that I like the most: http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html

Solution 2:[2]

I am dealing with the concept of functional programming for a while now [...] But there is one thing I do not get: How to deal with side-effects when restricting yourself to pure functions.

Claus Reinke posed a similar question when writing his thesis - from page 10 of 210:

How must interactions between a program and an external environment (consisting of, e.g., input/output-devices, file systems, ...) be described in a programming language that abstracts from the existence of an outside world?

For functional languages like Haskell which strive for the mathematical notion of a function, this brings up another question:

  • As mathematics also abstracts from the existence of an external environment (consisting of, e.g., input/output-devices, file systems, etc.):

    In a preliminary sense, mathematics is abstract because it is studied using highly general and formal resources.

    The Applicability of Mathematics (The Internet Encyclopedia of Philosophy).

    then why would anyone try using mathematics to describe interactions with an external environment, namely those which are supported by a programming language?

It just seems counterintuitive...


At a time when "traditional programming languages" were almost always imperative:

During the 1960s, several researchers began work on proving things about programs. Efforts were made to prove that:

  • A program was correct.

  • Two programs with different code computed the same answers when given the same inputs.

  • One program was faster than another.

  • A given program would always terminate.

While these are abstract goals, they are all, really, the same as the practical goal of "getting the program debugged".

Several difficult problems emerged from this work. One was the problem of specification: before one can prove that a program is correct, one must specify the meaning of "correct", formally and unambiguously. Formal systems for specifying the meaning of a program were developed, and they looked suspiciously like programming languages.

The Anatomy of Programming Languages (page 353 of 600), Alice E. Fischer and Frances S. Grodzinsky.

(emphasis added.)

Claus Reinke makes a similar observation - from page 65 of 210:

The notation for interactive programs written in the monadic style is irritatingly close to the notation used in imperative languages.

But there is still a possibility of success:

Researchers began to analyze why it is often harder to prove things about programs written in traditional languages than it is to prove theorems about mathematics. Two aspects of traditional languages emerged as sources of trouble because they are very difficult to model in a mathematical system: mutability and sequencing.

The Anatomy of Programming Languages (same page.)

("Very difficult", but not "impossible" - and apparently less than practical.)

Perhaps the remaining issues will be resolved at some time during the 2260s 2060s, using an extended set of elementary mathematical concepts. Until then, we'll just have to make do with awkward I/O-centric types e.g:

IO is not denotative.

Conal Elliott.


Since IO has (sort of) been explained elsewhere, let's try something different - inspired by Haskell's FFI:

data FF a b  -- abstract "tag" type

foreign import ccall "primArrFF"  arrFF  :: (a -> b) -> FF a b
foreign import ccall "primPipeFF" pipeFF :: FF a b -> FF b c -> FF a c
foreign import ccall "primBothFF" bothFF :: FF a b -> FF c d -> FF (a, c) (b, d)
foreign import ccall "primAltFF"  altFF  :: FF a b -> FF c d -> FF (Either a c) (Either b d)
foreign import ccall "primAppFF"  appFF  :: FF (FF a b, a) b
foreign import ccall "primTieFF"  tieFF  :: FF (a, c) (b, c) -> FF a b
                      ?

foreign import ccall "primGetCharFF" getChar :: FF () Char
foreign import ccall "primPutCharFF" putChar :: FF Char ()
                      ?

As for Main.main:

module Main(main) where

main :: FF () ()
       ?

...which can be expanded to:

module Main() where

foreign export ccall "FF_main" main :: FF () ()
       ?

(FF's instances for Arrow, et al are left as an exercise ;-)

So for now (2022 Jan) this how you can deal with side-effects when restricting yourself to ordinary Haskell functions:

  • Introduce an appropriate abstract type (FF a b) for entities with observable effects;
  • Introduce two sets of primitives - one set of combinators (arrFF, pipeFF, bothFF, altFF, appFF, tieFF, etc.) and one set of non-standard morphisms (getChar, putChar, etc);
  • You then define Main.main :: FF () () using ordinary Haskell functions and those FF primitives.

In this way, ordinary Haskell functions can remain free of side-effects - they don't actually "run" FF entities, but build them from other (usually smaller) ones. The only FF entity to be "run" is Main.main via its foreign export, which is called by the runtime system (usually implemented in an imperative language which allows side-effects).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Joni
Solution 2