Type Inferencing: Why it Matters
Author |
Message |
wtd
|
Posted: Wed Aug 10, 2005 3:13 pm Post subject: Type Inferencing: Why it Matters |
|
|
The recent lull here is begging for a new discussion, and for me to bash languages many of you adore.
Therefore let's discuss type inferencing, and why it matters, or rather, perhaps how lots of languages are wrong.
A simple example to start things out. This could be considered C, C++, or D.
code: | int fortyTwo()
{
return 42;
} |
And then assigning the result of that to a variable.
code: | int foo = fortyTwo(); |
I'd be hard pressed to think of a simpler example, which is why it's a good one.
What's wrong with it, though?
Well, look at those "int" declarations. Surely the compiler, especially with something so simple, can determine that the function fortyTwo returns an integer, and therefore that any variable that value is assigned to should be an integer as well.
And yet, despite the fact that it can figure that out, it doesn't use that information to help you.
Let's look at an example that does use that information to make things a bit easier. Copied and pasted from the SML/NJ toplevel interpreter.
code: | - fun fortyTwo () = 42;
val fortyTwo = fn : unit -> int
- val foo = fortyTwo ();
val foo = 42 : int
- |
Nowhere did the code I wrote (indicated by the "-" prompt) tell the compiler what type of data was being used. It determined that anyway, and showed me.
Of course, you're sure to say, doesn't using explicit type names prevent errors?
Let's look at some C++.
code: | int fortyTwo()
{
return 42;
}
int main()
{
string bar = fortyTwo();
return 0;
} |
Your C+ compiler will choke on this and give you an error, right? I mean, the return type of fortyTwo isn't "string".
SML/NJ, or any other type inferencing language won't balk at this, because it will infer that "bar" is an integer. I'm not using bar anywhere yet, so there's no error, and no need to report one.
If I attempt to use two values in inconsistent ways, then I get an error.
code: | - val baz = ref "Hello";
val baz = ref "Hello" : string ref
- !baz;
val it = "Hello" : string
- baz := bar;
stdIn:34.1-34.19 Error: operator and operand don't agree [tycon mismatch]
operator domain: string ref * string
operand: string ref * int
in expression:
baz := bar
- |
Or if I try to give "bar" to the print function, which expects a string.
code: | - print bar;
stdIn:36.1-36.10 Error: operator and operand don't agree [tycon mismatch]
operator domain: string
operand: int
in expression:
print bar
- |
And yet, I still haven't actually specified a type name in my code. Still, I have all of the benefits of type checking. |
|
|
|
|
|
Sponsor Sponsor
|
|
|
rizzix
|
Posted: Thu Aug 11, 2005 11:36 am Post subject: (No subject) |
|
|
Ok, but i think it's kind of redundant. But, only because I have a better idea!
Instead of handling it that way, what could have been done with the return type, is proper function overloading!
Let say I have the following function: code: | int getANumber() {
return 120;
} | Why not just use the return type to implement proper function overloading.. Such that, I can _also_ have a function like this: code: | float getANumber() {
return 125.0f;
} |
Then we can easily do this: code: | float f = getANumber();
int i = getANumber(); | and now when float f is assigned getANumber(), it uses the float-return-type overloaded function and thus have a value of 125.0f. similarly the int i will have a value of 120.
This IMO is a better use of the return type.
Now in the OOP world, the problem is ambiguity through inheritance.. For example: code: | Integer getANumber() {
return Integer.valueOf(120);
} | and code: | Float getANumber() {
return Float.valueOf(120.0f);
} |
Assuming Number is the parent of Integer and Float, it would be ambigious if I do something like this: code: | Number n = getANumber(); |
I'd suggest for ambigious such as these, force the programmer to specify which function he really meant to use, maybe through a syntax like this: code: | Number n = (Float)getANumber(); | or code: | Number n = Float:getANumber(); | or code: | Number n = getANumber() :: Float; | etc.. or which ever syntax seems appropriate for the language.
To eliminate an ambigious case, the progammer may also optionaly create a function like this: code: | Number getANumber() {
return (Float)getANumber();
} |
So.. how do u like my idea? |
|
|
|
|
|
wtd
|
Posted: Thu Aug 11, 2005 1:44 pm Post subject: (No subject) |
|
|
What if I have a function which is overloaded on its parameter to take either an Integer or a Float, and I pass it the result of getANumber?
There are very good reasons this has yet to find its way into even most academic languages.
Look at Perl6, though, if this interests you.
Also, you should look at Haskell's type classes. |
|
|
|
|
|
rizzix
|
Posted: Thu Aug 11, 2005 2:37 pm Post subject: (No subject) |
|
|
wtd wrote: What if I have a function which is overloaded on its parameter to take either an Integer or a Float, and I pass it the result of getANumber? good! another ambigious case.. and hence u use my solution to resolve ambiguity.
soo it would look something like this: code: | someOverloadedFunction((Float)getANumber()); |
But a _real_ argument would have been.. "wht's the point!" Why would u want to encourage poor programming practices.. If a function can be overload more than it is now.. i mean. .wow think about the weird results one could get from poorly written code.. (my above snippets are a great example.. getANumber() could give u 120 or 125 depending on the context through which it is called). Only a few cases can benefit from such a change..
And.. it destorys polymorphism! if i need to wirte: code: | someOverloadedFunction((Float)getANumber()); | then i might as well should have written: code: | someOverloadedFunction(getAFloatNumber()); | or code: | someNotSoOverloadedFloatFunction(getANumber()); |
Perl 5 already implements it to a small extent.. a sub{} can return different results depending on whether it's called in a list context or a scalar context. It works well there..but it does not work very well elsewhere.. |
|
|
|
|
|
wtd
|
Posted: Thu Aug 11, 2005 2:59 pm Post subject: (No subject) |
|
|
Much of the confusion arises from the moronic inclusion of automatic type conversion in some programming languages. |
|
|
|
|
|
rizzix
|
Posted: Thu Aug 11, 2005 3:02 pm Post subject: (No subject) |
|
|
yea.. i edited my post.. added another argument: it devalues polymorphism. |
|
|
|
|
|
wtd
|
Posted: Thu Aug 11, 2005 6:48 pm Post subject: (No subject) |
|
|
I suppose I should ask... how do you think type inferencing and polymorphism interact? |
|
|
|
|
|
wtd
|
Posted: Thu Aug 11, 2005 7:16 pm Post subject: (No subject) |
|
|
Oh, what the heck, I don't feel like waiting for an answer, because I'm an impatient hacker.
Let's compare Java and O'Caml. Both are high-level object-oriented programming languages. One uses explicit typing, and the other features type inferencing.
One of the key aspects of polymorphism is the concept of an interface.
code: | interface Foo
{
public void sayMessage();
}
class Bar implements Foo
{
public void sayMessage()
{
System.out.println("Hello");
}
}
public class Test
{
public static void main(String[] args)
{
say(new Bar());
}
public static void say(Foo f)
{
f.sayMessage();
}
} |
Or we could avoid the intermediate class.
code: | interface Foo
{
public void sayMessage();
}
public class Test
{
public static void main(String[] args)
{
say(new Foo
{
public void sayMessage()
{
System.out.println("Hello");
}
});
}
public static void say(Foo f)
{
f.sayMessage();
}
} |
But what's really happening here?
We're creating an interface named "Foo" which says that any object implementing that interface must have a "sayMessage" method. The "say" method then can only take in an object which implements a sayMessage method, meaning it implements the Foo interface.
But really, we can determine what methods any argument to "say" should have, and what methods objects do have, so isn't this all redundant?
code: | class bar =
object
method say_message = print_endline "Hello"
end;;
let say f = f#say_message;;
say (new bar) |
Or perhaps:
code: | let say f = f#say_message;;
say (object method say_message = print_endline "Hello" end) |
|
|
|
|
|
|
Sponsor Sponsor
|
|
|
wtd
|
Posted: Thu Aug 11, 2005 7:17 pm Post subject: (No subject) |
|
|
And yes, that looks like duck-typing in Ruby, but the difference is that all of the detection happens at compile time, before the program ever runs. |
|
|
|
|
|
rizzix
|
Posted: Fri Aug 12, 2005 1:08 am Post subject: (No subject) |
|
|
or maybe we're looking at it the wrong way.. maybe we don't need polymorphism.. it's simply a hinderance to type safety! meh! |
|
|
|
|
|
md
|
Posted: Fri Aug 12, 2005 9:16 am Post subject: (No subject) |
|
|
I think I'll just stick to my explicit type declarations fornow... I like being able to look at a function header and know right away what it returns, or what types it expects it parameters to be. Sure you could probably figure that out with inferenced types too, but when it's right there it's so much easier.
/me waits for wtd to show that that's not the case |
|
|
|
|
|
wtd
|
Posted: Fri Aug 12, 2005 1:56 pm Post subject: (No subject) |
|
|
rizzix wrote: or maybe we're looking at it the wrong way.. maybe we don't need polymorphism.. it's simply a hinderance to type safety! meh!
You can have both.
Of course, some Java apologists (not necessarily including you in that category) would like you to think otherwise. |
|
|
|
|
|
wtd
|
Posted: Fri Aug 12, 2005 2:04 pm Post subject: (No subject) |
|
|
Cornflake wrote: I think I'll just stick to my explicit type declarations fornow... I like being able to look at a function header and know right away what it returns, or what types it expects it parameters to be. Sure you could probably figure that out with inferenced types too, but when it's right there it's so much easier.
/me waits for wtd to show that that's not the case
Using short, concise functions with meaningful names takes care of most of what you're looking for.
Interactive interpreters can handle the rest.
For instance, let's say I define a function called sayHelloTo in SML/NJ.
code: | fun sayHelloTo name = print (name ^ "\n"); |
Now I can easily infer that since name is used in a string concatenation operation, it must be a string, but if I want to be sure... (copied from the interpreter)
code: | - sayHelloTo;
val it = fn : string -> unit |
"fn" indicates that it's a function. "string -> unit" tells me that it takes a string as an argument and returns "unit", or (). |
|
|
|
|
|
|
|