POV-Ray : Newsgroups : povray.off-topic : Haskell vs Java: Building a ray tracer : Re: Haskell vs Java: Building a ray tracer Server Time
29 Jul 2024 02:31:51 EDT (-0400)
  Re: Haskell vs Java: Building a ray tracer  
From: Orchid Win7 v1
Date: 28 Jun 2012 13:18:59
Message: <4fec9203@news.povray.org>
>> So how does all this look in Haskell?
>>
>> First of all, we need vectors.
>>
>> data Vector3 = Vector3 !Double !Double !Double
>>
>> class Num Vector3 where
>> (Vector3 x1 y1 z1) + (Vector3 x2 y2 z2) = Vector3 (x1+x2) (y1+y2) (z1+z2)
>> ...
>>
>> Similarly, we need colours. Unlike Java, we can actually use real
>> grown-up operator names, so we don't have to write monstrosities like
>>
>> return v1.Add(v2).Add(v3).Normalise();
>>
>> Instead we can simply say
>>
>> normalise (v1 + v2 + v3)
>>
>> which is far easier to read. It also means stackloads of existing
>> numeric functions automatically work with vectors (e.g., the sum
>> function can sum a list of vectors - or a list a colours, which turns
>> out to be more useful).
>
> Don't you still have to define how the sum function handles this Vector3
> object?

No. The sum function just calls the (+) function, so as long as (+) has 
been defined, sum will work.

(Actually, that's not completely true. sum also uses zero, so you have 
to define what a zero vector is. That's the rest of the Num instance 
that I left out with "...")

> Slight correction, in Java, you only define the constructor once, not at
> every call.

Well, yes. You have to write a constructor once per class, which means 
if you need a lot of classes, you end up writing a heck of a lot of 
constructors.

> Then,
>
> new Vector3(1, 2, 3)
>
> vs.
>
> Vector3 1 2 3
>
> Is only a matter of language syntax. You save 7 characters. Whooptidoo!

Sure. I'll give you that one.

>> Similarly, look back at the Java Ray class. See how much boilerplate
>> code there is for declaring that the fields are public and constant, and
>> for constructing a new object, and so forth. Now look at Haskell:
>>
>> data Ray = Ray {ray_start, ray_direction :: Vector3}
>>
>> ray_point :: Ray -> Double -> Vector3
>> ray_point (Ray s d) t = s + d |* t
>>
>> Much more compact, and yet much more /readable/ at the same time.
>
> Not quite. Since I don't speak Haskell, I wouldn't even know what two
> lines of codes actually mean. I kind of figure that the :: specifies
> that data type (ray_point is of type ray), but I have no idea why you
> have the "-> Double -> Vector3" added at the end. Likewise, not knowing
> what "!*" means makes the second line hard to follow.

Given that the equation of a straight line is f(t) = S + Dt, I would 
have imagined that s + d |* t would relatively stright-forward. (Oh, the 
puns...)

Do you not think that, once you've learned a little syntax, having to 
read just two lines of code is easier than having to wade through 12? (I 
guess that one isn't all that easy to answer objectively...)

>> Haskell's lazy evaluation makes this quite unnecessary. All we need is
>> /one/ function which returns all intersections in a lazy list.
>
> You mean like AllIsect() does in Java.
>
>> Inspecting whether the list is empty evaluates just enough of isect to
>> answer this question, and no more
>
> You already do it for AllIsect(). There's no additional work necessary.

In Java, calling AllIsect() causes all intersections to be computed. In 
Haskell, calling the isect function does /not/ necessarily cause all 
intersections to be computed. Only the ones you actually "look at". 
That's the point I'm making.

> How would the caller know how the size of the array?

Java stores the size of all arrays and lets you query it at run-time. It 
also throws exceptions on all array bounds violations. Java is 
supposedly a "safe" language, remember? (It also throws exceptions on 
all null pointer accesses, for example.)

>> In general, any Java class that has "just one method" can be turned into
>> a plain vanilla Haskell function. That means that Surface is just a
>> function, although we need to think carefully about what arguments it
>> needs. As it turns out, Surface needs quite a few items of data, all of
>> similar types. Since function arguments are unnamed in Haskell, it's
>> probably a good idea to define a data structure. (Otherwise we'll
>> forever be supplying the arguments in the wrong order and causing weird
>> bugs that the compiler can't catch.)
>
> So a simple language syntax is a good thing, except when it isn't! :P

Haskell doesn't have named arguments, so you can trivially define a data 
structure and solve the problem that way. It's no big deal.

Java doesn't let you mention arguments by name when invoking a function 
either. But if you have an IDE, it will at least show you what the 
argument names are in the function definition, which can be good 
enough... You're still likely to get it wrong, though. Java doesn't let 
you explicitly assign values to names at all, unless you start making 
public fields writable or manually writing lots of getter and setter 
methods.

> I don't know why you name your camera parameters vx, vy, vz. I guess I'm
> used to POV's location, up, right and look_at...

More like location, up, right and forward. If this wasn't example code, 
you would probably write down documentation for what all these 
parameters are actually supposed to mean, the assumptions about them 
(e.g., the code is assuming that some of these are /unit/ vectors, and 
others aren't), and so on.

>> Recall that in Java, ever single one of these things would be an entire
>> class, with the "public class Woo extends Wah" and the field declaration
>> and the constructor declaration, and only THEN do we get to writing the
>> bit of code that actually does something useful.
>
> No, you'd have one camera class, and have different calculations based
> on the value of the camera.type member

This is about as anti-OO as you can get.

> or two dereived classes that
> inherit from a generic camera class, if you prefer.

Which would be... one class per camera type, as I asserted. :-P

> Likewise, you'd have only one surface class with ambient, diffuse and
> reflect being member methods, with another method called FinalColour
> that combines the colour of those three and returns the overall colour
> obf the surface at that point.

Well, yeah, you could write one Surface class which implements every 
possible rendering model all at once. It's more modular to make a 
separate class for each term, but you could go down the monolithic route 
if you want.

>> In Haskell, we just write the useful bit. All of the stuff about is
>> useful code, and no filler. It's all stuff implementing actual maths,
>> not micromanaging object construction or field initialisation or
>> whatever.
>
> You don't need to micromanage object construction and field
> initialisation in Java or C++ either, but if you don't you better be
> sure that no one will ever try to access an uninitialized member!!!

In Haskell, you write down the name of the thing, what fields it should 
have, and what their types are. The end.

In Java or C++, you have to write whether each field is public or 
private, whether it's constant or not, you have to manually write the 
function that takes all the values and assigns them to each of the 
fields, and in general you have to write a whole /bunch/ of code to get 
the job done.

Then again, if you're working in Java or C++ (more so Java, since C++ 
isn't pure-OO), you shouldn't be writing too many classes which just 
passively hold on to some data. Objects are supposed to "do" stuff.

> By the way, what happens in Haskell if you try to access an unitialised
> member?

It throws an exception saying "this field is uninitialised". It also 
generates a compiler warning.

>> So if you can't access object internals, how can you implement spatial
>> transformations? Easy: You transform the coordinates before input.
>>
>> transform_texture :: Transform -> Texture -> Texture
>> transform_texture transform texture =
>> \ point -> texture (transform point)
>>
>> Now wasn't that easy?

> I think you have the whole idea backwards. Each object should know how
> it is being transformed (think about it, how else can you determine
> which face is being intersected by a ray?), therefore can take care of
> those transformations internally when computing the object.FinalColour
> value, so the transformation computations will have access to those
> private members, you don't need to do any of the gymnastics you just did.

Composing two functions is hardly "gymnastics".

Essentially, there are two ways you can implement a coordinate space 
transformation:

- Transform the object and leave the coordinates alone.
- Transform the coordinates and leave the object alone.

Both of these are equivalent, and equally valid, ways to solve the 
problem. The latter has the advantage that you don't have to reimplement 
the transformation code for every object; you can write it just once.

>> What have I just said? The short summary is that Haskell, the finest
>> functional programming language in the land, is superior to Java, one of
>> the more sucky OOP languages. Not exactly a revelation, is it? I think
>> I'm going to go outside for a while...
>
> No, you've just showed that trying to use one language in a way that is
> best suited for another language gets ugly.

Ray tracing is all about mathematical operations. Vector 
transformations, set unions, spatial subdivisions, etc. And Haskell 
makes it really, really easy to write that code. Java is completely 
capable of doing everything Haskell is doing. It just makes it much more 
work.

> the Lisp programmers had to write all kinds of additional
> code to turn off all kinds of bounds, checking, automatic type
> conversions, etc... just to come with comparable speeds.
>
> If you _know_ that you will never run out of bounds with a double or an
> int, then the C porgrammer will cliam victory. However, If you aren't
> absolutely sure that you can't end up with a negative orbit radius, or a
> heartrate larger than MAX_INT, then Lisp's automatic bounds checking
> will save you some time-consuming assert() calls and test cases.

...which is why I use a programming language that defaults to safe 
rather than fast?

(It still amuses me when Oracle claims that Java is a "safe language". 
Compared to Haskell, it's pretty damned weak, actually...)


Post a reply to this message

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