In this context, what's nondeterministic isn't the computation that Haskell is performing, but instead the computation that is being represented. When viewed as a monad (or applicative functor), lists represent nondeterministic computation: just as a Maybe a
is a computation of an a
that might have failed, or an IO a
is computation of an a
that did some I/O, a [a]
is a nondeterministic computation of an a
. Thus the list [1,2]
, under this interpretation, represents a computation that nondeterministically returns 1
or 2
, and similarly for [4,5,6]
. Or again, by way of analogy: computing Nothing
in Haskell succeeds, even though that value represents failure; computing [1,2]
in Haskell is deterministic (and pretty boring), but that value encodes a form of nondeterminism.
Thus, (+) <$> [1,2] <*> [4,5,6]
computes x + y
nondeterministically. Again, that's not what's written in the code – that's what the code represents. The code itself deterministically computes the representation of a nondeterministic computation!
The way this works is that <$>
and <*>
functions lift computations inside an applicative functor, so that snippet says to compute (+)
inside the list applicative functor, which means it computes (+)
nondeterministically:
[1,2]
represents a computation that could return either 1
or 2
. Call its result x
.
[4,5,6]
represents a computation that could return any of 4
, 5
, or 6
. Call its result y
.
- Thus, adding the results of those computations together – computing
x + y
– could evaluate to the sum of any of the possible values for x
and y
.
This is what the quote is saying, just in slightly more and different words :-)
In fact, (+) <$> [1,2] <*> [4,5,6]
is exactly equivalent to [x + y | x <- [1,2], y <- [4,5,6]]
, where the "nondeterminism" is instead x
and y
each iterating over their respective lists. This is all that's meant by nondeterminism, in the end!
As for how you were thinking about understanding this: remember, Haskell code is guaranteed to be deterministic in its results, thanks to Haskell's purely functional nature. The order of computation, however, doesn't affect this, so is left fairly unconstrained, as long as functions don't fail too early (e.g., const () undefined
must evaluate to ()
). We only get nondeterminism by representing it as an effect; lists are one encoding of this (and IO
can be another, for a very different kind of nondeterminism).