Programs that are meant to act as stream filters, taking data from some input source and progressively processing it and producing output, normally read from standard input when invoked with no non-option arguments. So for example
tail, etc. read from standard input by default. Tools like
sed, etc. require one operand (the regexp for grep, the script for sed) and read from standard input if that's the sole operand. Vi(m) does not fit this mold: it's an interactive program, not a stream filter, so it starts in interactive mode when you pass no argument.
file is an exception: it doesn't read from stdin unless stdin is given as an argument (possibly through the syntax
-). I don't know why neither the original author nor the POSIX committee decided that it wouldn't read from stdin. This may be because
file cares not only about file contents, but also about file types —
file foo reports whether
foo is a directory, a regular file, a symbolic link, etc. So it isn't quite a stream filter like the others, even though it's usually used as such. It's halfway between
grep (a stream filter) and
ls (which cares only about the file as a directory entry and not at all about the file contents).
grep -r does care about file types as well, but that's an addition that came much later than the original command.)