> return (list.Count == 0) ? null : list;
Would the C# compiler catch it or would you not find out until runtime? An ML compiler would tell you that function is wrong.
What about using Head? If you have a list of integers, will the compiler let you do:
> Head(integer_list) + 2
An ML compiler won't, because it's not safe.
> This isn't strictly the same as what you described if "None" is different from "null".
It is, but the important thing to note is I had to specify that the function, 'hd', can return 'null', by wrapping it in the option type. 'null' is only a valid value for option types. If I write:
> val hd : 'a list -> 'a
I could not return 'null'/None, it simply isn't valid. And the compiler wouldn't let me.
> the 'option' function you have in your example
'option' is a type, not a function (I suppose you can make some high level argument that all types are functions over values or something, but not relevant here). And 'option' itself isn't really even a type, it requires a type variable, so "'a option" is a type where "'a" is replaced by a concrete type at some point. It's not doing the work you described above, it's actually defining a contract between me and users of hd and the compiler.
> With your code, it's easy to break the hd function by changing the implementation to return a different type. You might not even notice the bug if the different type's implementation is close enough to the right one.
No, it isn't actually. If I change the return type the compiler will not compile my code.
> I changed a method to use an integer instead of a string
This cannot happen in ML, the compiler won't compile it because an integer isn't a string. What you described is not type inference, it's dynamic typing. The only danger of type inferencing in ML is annoying type errors during compilation. I'm not sure you actually grok what type inferencing is.
> It also makes refactoring a large project much more difficult in my opinion because there is no way to distinguish between specification and implementation. Consequently, changing your implementation runs the risk of breaking your specification without being informed.
Actually, refactoring code in ML tends to be pretty easy. If you change the type of something the compiler won't compile your code, so you know instantly where you messed up and can fix it. You can construct code to make this not work, but in general that isn't the case. See https://ocaml.janestreet.com/?q=node/101
> let the compiler do that I say!
Exactly, and an ML compiler definitely does more of this than the C# compiler. At a cost, of course.
Your post has several statements which suggest you are ignorant of ML and type theory. If you're interested in learning more, check out Benjamin Pierce's book "Types and Programming Languages". Or just spend a weekend with Ocaml or Haskell and prepare for the compiler to frustrate you with it's strictness :)