OCaml is an eager language unless directed otherwise. To create lazy values we use the lazy keyword, e.g., lazy (print_endline "foo") will create a lazy value of type unit Lazy.t that will print foo, once it is forced with Lazy.force.
All other values in OCaml are eager and are evaluated immediately. You got confused with the let .. in ... syntactic form.
The syntactic form
let <var> = <expr1> in <expr2>
first evaluates <expr1> and then evaluates <expr2> with the value of <expr1> bound to <var>. Finally, the result of <expr2> becomes the value of the whole form.
So when you say, let x = 2 in x + x you first evaluate 2 and bind it to x and then evaluate x + x with x=2, which is 4, so the value of the expression let x = 2 in x + x in 4. This is a pure expression, it doesn't define any functions and doesn't bind x in the global context.
Going back to your example,
let x = print_endline "foo" in x, x
we first evaluate <expr1>, which is print_endline "foo" that has the side effect of printing foo and returns the value () of type unit. Next, () is bound to x and the <expr2> is evaluated, which x,x, and which is equal to (),().
Your second example,
let x () = print_endline "foo" in x (), x ()
binds x to the function fun () -> print_endline "foo" since
let f x = <expr>
is the syntactic sugar for
let f = fun x -> <expr>
i.e., to a definition of a function.
Therefore, your expression first evaluates <expr1>, which is now fun () -> print_endline "foo" and binds it to x. Now you evaluate x (), x () and calls x () twice, both calls return ().
Finally, you said in your question that,
If I have this function: let x = print_endline "foo" in x,x;;
You don't have any functions there. Much like in let x = 2 in x + x, you are not defining any functions but just evaluate x+x with x bound to 2. After it is evaluated, there are no leftovers, x is not bound to anything. In C-like syntax, it is the same as
{
int x = 2;
x + x;
}
So if you want to define a function, you have to use the syntax
let <name> <params> =
<expr>
Where <expr> is the body of the function that is evaluated in a context where each parameter is substituted with its actual value (argument).
E.g.,
let print_foo () =
print_endline "foo"
or,
let sum_twice x y =
let z = x + y in
2 * z
I would suggest reading the OCaml Tutorial to get a better idea of OCaml syntax.