In a sense, the only real difference is that variables in imperative languages can change, while related names in functional languages cannot. But I think it is important to understand the semantic difference of a higher level between them and how we think of them differently.
In imperative languages, a variable is a thing in itself that includes meaning. They are often compared to boxes that contain something. The contents of the field can change and, semantically, the field has its own identifier.
Haskell names, on the other hand, are just labels for the value. You can use one or the other completely interchangeably. In reality, they cannot change.
Compare binding in Haskell to use names in languages like Java¹. They too cannot change, and you yourself do not think about it; they are simply the names for the method to which they are attached.
Here is a far-fetched example of the difference: imagine a function f that closes over the imperative variable x :
var x = 7; function foo() { console.log(x); }
In fact, x is just a name for 7 ... to x . What you have closed is the variable x , not its value 7 , so if it changes, the behavior of foo will also occur.
In Haskell, on the other hand, if you bind 7 to the name x and close it, then this is the same as closing over 7 .
let x = 7 in \ () -> x
does the same thing as
\ () -> 7
(which on its own does nothing but just 7 on its own, ignoring the problems of rigor).
¹ ignoring reflection and similar fraud