A 2-tuple is a collection of elements that may have nothing to do with each other; a pair associates the first element with the second. It’s typically used to relate dictionary keys with their respective values, for instance.
Regarding functionality differences, as their docstring tell, a Pair is treated as a single “scalar” for broadcasting operations.
The reason we need a difference is to facilitate broadcast. Broacasting over (1,2) applies to the two elements separately, but broadcasting treats 1=>2 as a scalar.
That’s not the right question in Julia. All 2-element collections could do the same thing.
But, of course, you may not want them to do the same thing, and having a different type allows different semantics using dispatch. Specifically, key => value is used extensively in Julia.
Of course pairs and 2-tuples are structurally the same, and 2-tuples can just as well represent an association from the first item to the second item (that is the definition of a mathematical functional relation after all: [1], [2], [3], [4], [5], [6]).
The reason for the distinction is to clarify our intentions about using them: you use Pair when you want to represent a functional relation, you use Tuple when you want an ordered container. This way you reduce the chances of messing things up inadvertently.
It is same reason why you might want to introduce two structurally equivalent types
struct Meters{R<:Real} val::R end
struct Kilograms{R<:Real} val::R end
just so that you cannot accidentally confuse them and add Meters(3.14) + Kilograms(42.).
The newtype pattern ([7]) is based on this principle: you want a type which has the same underlying representation, but can behave differently.