Monomorphism, monomorphism...
Juan Carlos Arévalo Baeza
jcab@roningames.com
Sat, 6 Oct 2001 22:22:24 -0700
On 6 Oct 2001 09:31:54 GMT, Marcin 'Qrczak' Kowalczyk wrote:
First of all, thanks a lot for the detailed response.
>Yes, ... yes ... yes
Well, I'm glad I got some of it right. Gives me hope :)
>>>"contains a pattern binding with a nonvariable pattern"
>>
>>=A0=A0Meaning... what exactly?
>
>A pattern which is something other than an identifier.
Like defining a function, as opposed to defining a constant?
>>=A0=A0Hmmm... This still sounds like nonsensical (as in=
counterintuitive
>>=A0=A0and artificial) to me. In a definition like "let g =3D isNil"
>>=A0=A0there cannot be any compelling reason to give "g" any type
>>=A0=A0different than the type of "isNil".
>
>There are two reasons for the monomorphism restriction:
>
>- isNil (or any value with a non-empty context in its type)=
doesn't
>=A0really behave like a constant, but as a function taking a=
dictionary
>=A0of 'IsNil a' as an implicit argument, so it's recomputed each=
time
>=A0it it used...
Ok... This is an angle I hadn't even approached yet. We're=
talking about the internal implementation of the compiler here.=
Hmmm...
Shouldn't the compiler be able to limit the recomputations to=
one per instance of the class? That sounds perfectly appropriate=
to me, and it would solve this particular problem. Unless I'm=
missing something here...
>=A0This point is silly in this case, because it is already a=
function
>=A0with an explicit argument! It makes more sense however when the=
type
>=A0is not of the form a->b. For example in 'n =3D 7*11*13' letting n=
have
>=A0the type 'Num a =3D>=A0a' implies that it is recomputed each time=
it
>=A0is used.
Again, what about recomputing it once per instance of Num?
> Unless the compiler manages to optimize this, but it can't
>=A0in all cases (n can be used with different types in various=
places).
I guess I'm biased here by my knowledge of templates in C++,=
which can be used to implement something very similar to type=
classes in Haskell. This would be akin to different=
instantiations of a single template, which should not present=
any problems for the compiler.
So, 'n =3D 7*11*13' would have type 'Num a =3D> a', which would=
mean that it has a different value for each instantiation of the=
context 'Num a'. So, where's the problem? It's not as if Haskell=
didn't already have this functionality in the members of a type=
class:
class Num a where
n :: a
n =3D 7*11*13
'n' here has that type 'Num a =3D> a', doesn't it? Don't tell me=
compilers will compute it twice if we use it twice, as in:
n1 =3D n
n2 =3D n
>- When a pattern binds several variables, it can happen that=
their
>=A0types need different sets of class constraints. Using such a=
variable
>=A0doesn't fully determine the type to perform the computation=
in.
>=A0It's thus ambiguous and an error.
You're talking about the case '(var1, var2) =3D expr', right?=
That's because var1 and var2 cannot have different contexts?=
That still sounds unnecessary to me. I mean, the tuple result=
should have its own context, necessary to resolve the tuple=
itself, and each of its elements should acquire its own context=
as appropriate. Then all contexts should be unambiguous. In this=
case, 'expr' should be able to have type:
expr:: <context0> =3D> (<context1> =3D> TypeOfVar1, <context2> =3D>=
TypeOfVar2)
I'm guessing from what I learn that this is not possible in=
Haskell, right?
>=A0It's not ambiguous with monomorphic restriction, where all uses=
of
>=A0all variables contribute to finding the single type to perform=
the
>=A0computation in.
Exactly. I think I'm starting to get a reasonable handle on=
this.
>=A0Even with a single variable it can happen that some uses will
>=A0constrain the type enough to determine the instance, and some=
will
>=A0not. Without monomorphic restrictions some uses are OK, but=
others
>=A0will stop the compilation. With monomorphic restriction all=
uses
>=A0contribute to a single type and typing might succeed.
So it is. I just tried, with Hugs:
---
intToBool :: Int -> Bool
intToBool a =3D a =3D=3D 0
numToBool :: Num a =3D> a -> Bool
numToBool a =3D a =3D=3D 0
h :: Num a =3D> a
h =3D 4
g =3D h
f =3D (intToBool g) && (numToBool g)
---
It complained with:
---
ERROR E:\JCAB\Haskell\TestMonomorphic.hs:14 - Type error in=
application
*** Expression : intToBool g
*** Term : g
*** Type : Integer
*** Does not match : Int
---
You're saying that this is because, when seeing the line 'g =3D=
h', the compiler immediately and arbitrarily picks a type for=
'g', right? In this case, it's 'Integer'. Arbitrarily but not=
randomly, right? What rules does it follow?
>The general trouble with monomorphic restriction is that let=
bindings
>can mean two similar things:
>
>1. Create a single lazily evaluated object with the given=
definition
>=A0and make it or its components available through variables=
(pattern
>=A0binding).
>
>2. Define a possibly polymorphic function and perform the given
>=A0computation when it is applied (function binding).
In case 2 we wouldn't need the restriction, right?
I guess the only benefit from all this is efficiency, then. I=
think I see it. Back to the dictionaries... the dictionary for a=
type class is closed as soon as the definition of that class is=
closed. You can't add any new free functions to that dictionary.=
Therefore, the lookup for those functions would need to follow a=
much slower path. Is this accurate? I can see ways to overcome=
this limitation in a compiler, but it would need to make use of=
some deferred compilation, just like "export"ed templates in=
C++, where the linker has to be able to compile instances of=
templates. And support for "export" in most current C++=
compilers is far from ideal. And if C++, with all its extensive=
use doesn't have it, how could possibly Haskell have it?
>>>but two legal possibilities are
>>>- forall b . [b] ->=A0Bool, and
>>
>>=A0=A0Choosing an explicit instance of IsNil. But this sounds
>>=A0=A0nonsensical to me, too. No instance should be choosing unless=
the
>>=A0=A0specific instance type is forced by the definition.=
Otherwise,
>>=A0=A0if there are two insances, which one would it choose?
>
>Type inference defined in the traditional way is=
non-deterministic
>in nature.
>
>A good property of a type system is that it should not matter=
when
>various choices are made: as long as we succeed in deriving a=
type
>of the whole program, the overall meaning should be=
unambiguous.
>This property has a name - sorry, I forgot which.
>
>When we choose an instance too early, we might not succeed at=
all (if
>another instance happens to be needed). Unfortunately in this=
case we
>may fail in either case: no choice is sure to be better than the=
other.
>
>It might happen that by choosing this instance now it will be OK=
when
>f is used. The other possibility of the type for f would take=
away
>some freedom: we can't use this instance later with different=
choices
>for types of list elements, because both uses of g must make the=
same
>choices for type variables with class constraints. It's not=
visible yet
>(before choosing the instance) that the single type 'IsNil a=
=3D>=A0a'
>will expand into something containing quantified type variables=
without
>class constraints which can be instantiated independently!
Hmmm... As in typing '2+2' in Hugs and having it say=
'ambiguous type: Num a =3D> a'? Is this the kind of problem that=
you're talking about?
Hmmm... I begin to see the real problem. If we have a program=
such as:
---
g :: Num a =3D> a
g =3D 0
main =3D print g
---
The compiler MUST choose an instance for the overloaded=
'print' (must choose a concrete type for 'g'), because the=
program is ambiguously typed otherwise.
>Of course it may also happen that completely different instances=
will
>be needed when f is used, so choosing the instance now is bad.=
That's
>the point: there is no universal choice. The property of=
principal
>types would allow us to derive some most general type of f, such=
that
>any other type is its instance. Unfortunately it doesn't hold.=
This
>case was not expected.
The thing is that in some instances, the compiler chooses a=
bit too early. For example:
---
h :: Num a =3D> a
h =3D 0
g =3D h
---
Now 'g' is of type 'Integer' in Hugs, even though it doesn't=
need to. 'Num a =3D> a' would be more correct.
Same with functions:
---
h :: Num a =3D> a -> a
h t =3D t
g =3D h
---
Now 'g' is of type 'Integer -> Integer', instead of 'Num a =3D>=
a -> a'.
That's what I call "too early".
Thanx a bunch.
Salutaciones,
JCAB
email: jcab@roningames.com
ICQ: 101728263
The Rumblings are back: http://www.JCABs-Rumblings.com