So, you think that the following function is impure.
const pureHttpCall = (url, params) => () => $.getJson(url, params)
That's a reasonable thought. However, would you consider the following function pure?
const pureHttpCall = (url, params) => ({ url, params })
The second function doesn't make any HTTP call. It just returns a pure computation object.
You can eventually use this pure computation object to run impure code. For example, consider.
const runIO = ({ url, params }) => $.getJson(url, params)
const pureHttpCall = (url, params) => ({ url, params })
const computation = pureHttpCall("example.json", {}) // create a pure computation
runIO(computation) // use the pure computation to run impure code
Think of the pure computation object as a description of impure code. The description is pure but the thing it describes is impure. We're using a computation object data structure as a description for impure code.
Now, a function is also a data structure. After all, functions are first-class values in FP. Hence, instead of using an object as the description data structure, we can use a nullary function as follows.
const runIO = computation => computation()
const pureHttpCall = (url, params) => () => $.getJson(url, params)
const computation = pureHttpCall("example.json", {}) // create a pure computation
runIO(computation) // use the pure computation to run impure code
To summarize, $.getJson(url, params)
is an impure HTTP call. However, () => $.getJson(url, params)
is a pure description of an impure HTTP call.
If the fact that the description is an impure function is what's bothering you, then think of the function as a pure computation object like I showed above.
Converting functions into data structures like I did above is actually a compiler optimization technique known as defunctionalization.
This is how purely functional languages like Haskell deal with impurity. They wrap impure code in pure IO
actions. An IO
action is a description of impure code. Internally the IO
action is defined by a state monad data structure where the state is the RealWorld
[1].
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
The general idea is that you build a description of impure code (i.e. an IO
action) and then when you run the program the Haskell runtime will run the main
action. You can also use unsafePerformIO
to run IO
actions beside main
, and it is usually used to get configuration data.
Note that there's no such thing as “compile-time purity” and “runtime impurity” as @bob's answer says. Separation of purity and impurity is done by pure data structures which describe impure computations. You can do that in interpreted languages like JavaScript too as I showed above.
Compile time and runtime do not separate purity from impurity. Compile time simply refers to operations performed by the compiler, such as macro expansion or type checking. Similarly, runtime refers to operations performed by the program. The compile time of a program is the runtime of the compiler.
Note that parts of the program can also be executed at compile time. For example, function inlining and macro expansion is userland code being executed at compile time. In dependently typed languages, parts of the program are also evaluated at compile time for the purpose of type checking.