One issue that I found when solving problems like this is that, to achieve speed, it can become necessary to use many nested lets. That could hurt readability. Nobody wants to see a line of code nested 20 tabs deep!
and then express amb-like computations in effectively the same way:
sample :: [(Int, Int)]
sample = do
x <- [1,2,3,4,5]
y <- [1,2,3,4,5]
require (x == 2 * y)
return (x, y)
after which the value of sample is [(2,1),(4,2)]. (Also, my require is effectively the same as the guard function found in Control.Monad, specialized for lists.)