POV-Ray : Newsgroups : povray.off-topic : Parametric polymorphism Server Time
29 Jul 2024 06:22:12 EDT (-0400)
  Parametric polymorphism (Message 1 to 10 of 19)  
Goto Latest 10 Messages Next 9 Messages >>>
From: Invisible
Subject: Parametric polymorphism
Date: 10 May 2012 10:04:38
Message: <4fabcaf6$1@news.povray.org>
There is a saying about Haskell: "When it compiles, it usually works 
correctly first time."

In solemn truth, there are plenty of examples of Haskell code that 
compiles just fine, but doesn't work correctly. And yet, you'd be 
surprised how often that "usually" proviso holds.

(I also can't emphasise enough how many times I've written and rewritten 
code, only to find out the type signatures don't line up, and in the 
process realised that there's something fundamentally "wrong" with what 
I'm trying to do. The compiler has almost caught DESIGN ERRORS at 
compile-time!)

Another commonly heard statement is: "Sometimes you can tell WHAT A 
FUNCTION DOES just by looking at its TYPE SIGNATURE."

This hints at why the first statement is true: If there really /is/ a 
deep connection between what a piece of code does and what its type 
signature says, then it makes sense that type-checking the program is 
going to find a hell of a lot of bugs for you at compile-time.

But why does all this work in Haskell, and not in (say) C++ or Java or 
VisualBasic?

The short answer is "parametric polymorphism".



The idea is pretty simple. If I'm moving some data from A to B, I don't 
have to know OR CARE what type of data it is.

Let's consider a very simple example:

   extract :: [x] -> x

This says that the "extract" function takes a list of values as its 
argument, and returns one value as the result. The list can contain 
values of ANY TYPE, and the value returned is of THE SAME TYPE as the 
type of the list elements.

Let's consider implementing this function in C. Well, first of all, the 
only way to implement "any type" in C is with a void pointer. (Or a 
preprocessor macro, but that's just frigging EVIL!) So it would be a 
function that takes a container of void pointers and returns a void pointer.

What might this function do? Well, it could return the first element of 
the container. Or the last one. Or the middle one. Or the Nth one (if 
there are N elements). But it could also do several other things:

- Query a global variable to decide which element to return.
- Return a null pointer.
- Call malloc to generate a new pointer [to gibberish].
- Pluck a random integer out of thin air, and cast it to a pointer.
- Examine the data pointed to, and decide what to do based on the bit 
strings it finds there. [Obviously it can't know what they MEAN.]
- Modify the data pointed to.
- ???
- PROFIT!

In short, this function could do more or less /anything/.

Now let's consider the same thing in C++. Well, you could implement it 
the same way as above, but more likely it would be a template function. 
So what might that do? Well, again, it can return one of the elements of 
the container. But it can also do things like conjure up some random 
data and type-cast it to the result type. Or inspect global variables to 
decide what to do. Or analyse the bit strings of the argument to decide 
what to do.

I won't pretend to be a template expert, but as I understand it, a 
template is allowed to call any function that exists. (With the proviso 
that when the template is compiled, the result might not type-check.) So 
our extract() function template might search the container for the 
highest or lowest element (and fail to compile when the element type 
doesn't support order comparisons).

C and C++ are both quite low-level languages. The emphasis is more on 
allowing the programmer access to every possible ability of the 
hardware, rather than on guaranteeing sane operation.

So how about Pascal? [Not that anybody writes production code in Pascal 
any more.] Well, the situation is similar to C. [Not that every Pascal 
dialect supports untyped pointers. If not, a few type casts will fix that.]

OK, so let's try Java. OK, well the closest Java can manage to "any 
type" is the Object class. (If your data is a primitive type like int, 
you'll have to wrap it in an object. Way to support efficient 
primitives, Java! :-P ) Otherwise, the situation is much like C++. In 
particular, you can downcast your objects to any class in the system.

Unlike C or C++, this is CHECKED at run-time, so it can fail. This 
raises the possibility of trying lots of downcasts to figure out what 
the actual class of the object is. C++ can do the same thing with 
multiple templates.

Actually, Java has the reflection API, so you can actually /look up/ the 
exact class at runtime. And look up what methods it has, and call one of 
those. So here, you can /literally/ do /anything/!



So how about Haskell?

OK, well /obviously/ Haskell doesn't have global variables. So the 
output of "extract" can /only/ depend on its input. And the output will 
always be the same for a given input. But we know that already.

Haskell (or rather, GHC) /implements/ polymorphism using pointers. But, 
much like Java, this isn't visible at the programmer level. (In 
particular, no pointer arithmetic. No casting integers to pointers. No 
allocating memory without knowing its type.)

But here is the huge, huge difference. I will say this very slowly:

   Haskell DOES NOT HAVE type casts.

There is literally no way I can take some random garbage and cast it to 
the correct result type. It's not possible. So the ONLY way to construct 
a value of a specific type is... to know what that type is.

A polymorphic function has NO IDEA what type of data it is dealing with. 
There is NO WAY to find out. Put simply, you cannot create data of a 
polymorphic type.

And THAT means that "extract" cannot CREATE a result value out of 
nothing. It has to OBTAIN one from somewhere. And since the only place 
it has access to is the list passed in as an argument, the language 100% 
guarantees that the result returned from "extract" is definitely one of 
the elements of the input list.

In other words, the type signature of a function in C or C++ or Java or 
whatever tells you next to nothing about what the function does. But in 
Haskell, a function type can tell you WHAT THE FUNCTION DOES.

Notice that we don't know WHICH element extract returns. Maybe it's the 
first one. Maybe it's the last one. Maybe its one in the middle. It 
definitely ISN'T the lowest or highest element. You know why? Because 
not all types have order comparisons, and the type signature doesn't 
specify that the result type has order comparisons. Which means that 
extract is not allowed to use comparison operators.

If we wrote

   extract :: Ord x => x -> [x]

then extract WOULD be allowed to do order comparisons. So that would 
tell us something ELSE about what the function does. (It doesn't HAVE TO 
use order operators - but a "sensible" programmer wouldn't add an order 
constraint unless the function actually requires one. Admittedly we're 
now dealing with common sense rather than absolute proof...)

If I give you a value of type "x", the ONLY thing you can do with that 
value is move it around and copy it. You cannot do ANYTHING ELSE with 
it. You can't create new values of that same type. You cannot inspect or 
alter the value I gave you. You cannot do anything, except move it around.

If I give you a value of type "x" and a function that takes type "x" as 
argument, then you can apply the function to the value and do something 
with the result. Or you can move the value and/or the function around a 
bit. And that is all.

If I give you a value of type "x" which is a member of class "y", then 
you can apply any method of class "y" to the value. (And move the value 
around, as before.)

If I give you a value of type "Int", you can apply any function in the 
system that expects Int values. (There are quite a lot of these.) But if 
I give you a POLYMORPHIC type, then you were severely constrained in 
what you can do with it. The above rules show you that.

Taking this into account, the following is impossible:

   convert :: x -> y

This type signature says that "convert" accepts a value of ANY TYPE and 
returns a value of ANY OTHER TYPE. This is quite impossible in Haskell.

Some people make the mistake of thinking that if a function returns a 
polymorphic type, that means that THE FUNCTION can decide what actual 
type it feels like returning. For example, in Java a method can return 
"Object", and then the actual method code decides which subclass of 
Object to actually return.

Haskell does not work like this. In Haskell, THE CALLER decides what the 
return type should be, not THE CALLEE. This means that any function 
returning a polymorphic type must be capable of generating ANY POSSIBLE 
TYPE as result - and that is only possible if one or more of the 
function's argument provide a way to generate such a value.



In a sense, every Haskell type signature is a logical theorem, and the 
corresponding source code is the proof. And now I understand how: Each 
type name (polymorphic or not) stands for the statement "this type is 
inhabited". A type is "inhabited" if a value exists which has that type. 
And the source code of the function is the "proof" in that it shows you 
how to /construct/ such a value.

Consider again the type "x -> y". This type is uninhabited. It is 
IMPOSSIBLE to construct a value (i.e., a function) with this type 
signature. On the other hand, the type "x -> x" is inhabited. It has 
exactly one inhabitant: the identity function.

The type signature "x -> y" can more simply be interpreted as "given 
that X is true, it follows that Y is true". This is nonsense, since the 
statements are unrelated. On the other hand, "x -> x" reads as "given 
that X is true, it follows that X is true". Well, duh. Obviously.

Let's try the type signature for map:

   map :: (x -> y) -> [x] -> [y]

Literally: "Given that there exists a function from X to Y and there 
exists a list of Xs, it follows that there exists a list of Ys." Stated 
in this language, it is /obviously/ true, and it is /obvious/ that the 
source code for the map function constitutes a mathematical proof of 
this statement - for it is a constructive proof!

(Anyone paying attention may have noticed that I said "x -> y" was an 
impossible type, and yet that's the type of map's first argument! What 
you have to understand is that every Haskell type implicitly has an 
invisible "forall" over every type variable. Therefore,

   forall x, y: x -> y

is an impossible type. However,

   forall x, y: (x -> y) -> [x] -> [y]

is perfectly possible. The former demands a function that can turn any X 
into any Y - an impossible feat. The latter says that IF there is a 
function that can turn an X into a Y, THEN we can turn a list of Xs into 
a list of Ys.)



Incidentally, here is an interesting thing: The map function is 
polymorphic. But if I write, say, map toUpper, the toUpper function 
accepts only characters and returns only characters. So clearly, the 
next argument must be a list of characters, and the result must also be 
a list of characters.

Basically, the FUNCTION may be polymorphic, but every CALL to the 
function involves fixed types. If the calling function is itself 
polymorphic, then you can trace that out to the next step. In summary, 
if you take any valid Haskell program and analyse the entire program 
text, you can always statically compute the exact type of everything in 
the program.

In a sense, this is pretty damned similar to C++ templates. All the 
polymorphism is at compile-time. As I understand it, C++ implements 
templates by GENERATING new code for every combination of types that the 
template is applied to. Haskell (or rather, GHC) doesn't implement it 
that way. Instead, it dangles everything off of pointers. (Because, 
let's face it, every pointer has the same structure.) But it COULD 
implement it the way C++ does, and it would still work.

(Actually, there's a "specialize" pragma that makes GHC do exactly this, 
although only for the requested types. For example, you might want to 
optimise the map function when it's working on lists of integers. 
There's no real benefit to that, but some more complicated function 
might do something that benefits from a special-case.)


Post a reply to this message

From: Le Forgeron
Subject: Re: Parametric polymorphism
Date: 10 May 2012 10:41:19
Message: <4fabd38f@news.povray.org>
You will be delighted by the C++11 range-based for loop:

for(auto i : whateverSTL)
{
  // here you have all the element of the container as i, one at a time

}


no need for template, just basic code.

-- 
Real software engineers work from 9 to 5, because that is
the way the job is described in the formal spec.  Working
late would feel like using an undocumented external procedure.


Post a reply to this message

From: Invisible
Subject: Re: Parametric polymorphism
Date: 11 May 2012 10:13:38
Message: <4fad1e92$1@news.povray.org>
On 10/05/2012 03:41 PM, Le_Forgeron wrote:
> You will be delighted by the C++11 range-based for loop:
>
> for(auto i : whateverSTL)
> {
>    // here you have all the element of the container as i, one at a time
>
> }
>
>
> no need for template, just basic code.

STL?

As in Standard TEMPLATE Library?

Yeah, no templates here. ;-)


Post a reply to this message

From: Invisible
Subject: Re: Parametric polymorphism
Date: 11 May 2012 10:23:14
Message: <4fad20d2@news.povray.org>
On 10/05/2012 03:04 PM, Invisible wrote:

> Basically, the FUNCTION may be polymorphic, but every CALL to the
> function involves fixed types. If the calling function is itself
> polymorphic, then you can trace that out to the next step. In summary,
> if you take any valid Haskell program and analyse the entire program
> text, you can always statically compute the exact type of everything in
> the program.
>
> In a sense, this is pretty damned similar to C++ templates. All the
> polymorphism is at compile-time.

On closer inspection, this isn't *quite* true...

The counter-example I was shown is this:

   wrap :: Show x => Int -> x -> String
   wrap 0 x = show x
   wrap n x = wrap (n-1) [x]

This is "polymorphic recursion". The function recursively calls itself 
with a different type. Basically, it takes some data and wraps it in N 
layers of lists. And since N can only be known at run-time, you can't 
statically say how many times you need.

Actually, if you're paying attention, you'll notice that if N is 
*negative*, this yields an infinite loop, requiring infinity different 
type instanciations.

(And if you're *really* paying attention, you'll notice that Int has a 
finite number of bits in it...)

This example is fairly stupid of course - why would you want to wrap a 
value in a list of lists of lists? But it demonstrates an interesting 
edge case.

Notice that since the final type of X is determined by N, the function 
cannot *return* X, since there would be no way to write a sensible type 
signature for that. It can only apply a polymorphic function to X and 
return the result from that. (In this case, the "show" function, which 
works for any degree of wrapping.)

IMHO, all of this is probably of theoretical interest only. I can't 
imagine there being much practical use for it...


Post a reply to this message

From: Le Forgeron
Subject: Re: Parametric polymorphism
Date: 11 May 2012 10:53:00
Message: <4fad27cc@news.povray.org>
Le 11/05/2012 16:13, Invisible a écrit :
> On 10/05/2012 03:41 PM, Le_Forgeron wrote:
>> You will be delighted by the C++11 range-based for loop:
>>
>> for(auto i : whateverSTL)
>> {
>>    // here you have all the element of the container as i, one at a time
>>
>> }
>>
>>
>> no need for template, just basic code.
> 
> STL?
> 
> As in Standard TEMPLATE Library?
> 
> Yeah, no templates here. ;-)


I was thinking: no need to write your own template (with bug).


Post a reply to this message

From: Warp
Subject: Re: Parametric polymorphism
Date: 11 May 2012 10:57:15
Message: <4fad28ca@news.povray.org>
Invisible <voi### [at] devnull> wrote:
> On 10/05/2012 03:41 PM, Le_Forgeron wrote:
> > You will be delighted by the C++11 range-based for loop:
> >
> > for(auto i : whateverSTL)
> > {
> >    // here you have all the element of the container as i, one at a time
> >
> > }
> >
> >
> > no need for template, just basic code.

> STL?

> As in Standard TEMPLATE Library?

> Yeah, no templates here. ;-)

  The range-based for loop works with static arrays, initializer lists
and any object that either has a begin() and an end() member function
that return forward iterators, or which have std::begin() and std::end()
equivalents. Templates are not a requirement.

  The range-based for is especially designed to traverse STL data
containers, but them being templated has nothing to do with it.

-- 
                                                          - Warp


Post a reply to this message

From: Darren New
Subject: Re: Parametric polymorphism
Date: 12 May 2012 17:09:58
Message: <4faed1a6$1@news.povray.org>
I'll say again, this is very interesting. I would have been fascinated by a 
lecture this well put together in graduate school. :-)

-- 
Darren New, San Diego CA, USA (PST)
   "Oh no! We're out of code juice!"
   "Don't panic. There's beans and filters
    in the cabinet."


Post a reply to this message

From: Orchid Win7 v1
Subject: Re: Parametric polymorphism
Date: 13 May 2012 09:44:23
Message: <4fafbab7$1@news.povray.org>
On 12/05/2012 10:09 PM, Darren New wrote:
> I'll say again, this is very interesting. I would have been fascinated
> by a lecture this well put together in graduate school. :-)

It's always nice to be appreciated... In my line of work, this is a very 
rare occurrence, sadly.


Post a reply to this message

From: andrel
Subject: Re: Parametric polymorphism
Date: 13 May 2012 16:48:26
Message: <4FB01E1A.8060108@gmail.com>
On 13-5-2012 15:44, Orchid Win7 v1 wrote:
> On 12/05/2012 10:09 PM, Darren New wrote:
>> I'll say again, this is very interesting. I would have been fascinated
>> by a lecture this well put together in graduate school. :-)
>
> It's always nice to be appreciated... In my line of work, this is a very
> rare occurrence, sadly.

Then you are not in the right line of work.
...
Ehm, this smells of deja vu.


-- 
tip: do not run in an unknown place when it is too dark to see the 
floor, unless you prefer to not use uppercase.


Post a reply to this message

From: Stephen
Subject: Re: Parametric polymorphism
Date: 13 May 2012 17:02:03
Message: <4fb0214b@news.povray.org>
On 13/05/2012 9:48 PM, andrel wrote:
> Ehm, this smells of deja vu.

Deja eperlan? (Pardon my google ;-) )

-- 
Regards
     Stephen


Post a reply to this message

Goto Latest 10 Messages Next 9 Messages >>>

Copyright 2003-2023 Persistence of Vision Raytracer Pty. Ltd.