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:35:41 EDT (-0400)
  Re: Haskell vs Java: Building a ray tracer  
From: Francois Labreque
Date: 28 Jun 2012 15:37:43
Message: <4fecb287$1@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.

Makes sense.

>>> 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...)

Ok.  I Still don't know what the -> thingamadoodle does.

>
> 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...)
>

My point was that you don't have to wade through the constructor(s) and 
member function definition to understand what

return v1.Add(v2).Add(v3).Normalise();

does either. [as long as your method names aren't too obfuscated, of course]

Besides a lot of people gripe about Java not supporting operator 
overloading.  You are not the only one.

>>> 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.

As Clipka pointed out, you need to calculate all intersections anyway to 
find the nearest one.

>
>> 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.)
>

Thanks.  That's why I asked if there was automatic bounds-checking.

>>> 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.

[macho programmer mode=on]
IDEs are for sissies.  In my days, we programmed in vi and we liked it. 
  If we weren't sure about the order of parameters, we'd open the .h in 
another buffer and search for the function, or look at the man page for 
standard stuff.
[macho programmer mode=off]

Actually, we'd burn out the SHIFT-F1, or CTRL-F1, or whatever the hotkey 
was in Borland's Turbo IDE!

>
>> 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.
>

I was just pulling your leg.  You can call your variables Fred, Pebbles 
and Bambam if you want to.  Just don't go about making readability 
claims, if you do, though. :)

>>> 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.
>

So if I want to desing a red/black binary tree, I'd have to define a red 
class and a black class, instead of having colour as a member value of 
my node class?

The type of projection, be it isometric, orthographic, perspective, 
wall-eye, etc... only affects the direction the rays shot.  It's a lot 
simpler to have a switch{ } statement in one method than to reinvent 
five or six classes just because one of the methods is different (cf. 
you earlier comment about "one-method-classes").

If you are going to publish your stuff as a libray and expect people to 
build on it, then, of course, it makes sense to create new classes for 
each camera type derived from a common one, making it easier for someone 
to come along and add new camera types (e.g.: red/green stereoscopic)

>> 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
>

Agreed. However, you don't need to rewrite the constructor(s) and other 
methods, unless you really need to.  So, in the end, it just amounts to 
the syntactic differences between the two languages.

>> 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.
>

I fail to see in which circumstance, you'd need to know anything other 
than the final colour of an object, so I don't see the advantage fo 
computing the ambient, diffuse and reflective components of a point 
outside of the object itself.  Again, feel free to correct me if I'm 
overlooking something obvious, such as radiosity.  I'm not an algoritm 
expert.

>>> 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.
>

public/private/protected is important when you make libraries that are 
intended to be reused.  This is to prevent other programmers from 
mucking around with the internals of your objects and potentially 
creating all kinds of problems at runtime, such as memory leakage, or 
buffer overflows.  My OO theory is very rusty, but back in the day, 
objects were supposed to be treated as black boxes and you only 
interacted with them via public or protected methods.  Leaving 
everything wide open means you can either shoot yourself in the foot, or 
the compiler needs to add a lot of baby-sitting code that will prevent 
you from doing it at run-time (cf. my earlier anecdote about Lisp). 
This may not be all that important on a Win/Mac/*NIX machine with 8GB of 
ram, but in the mobile app area, the added bloat is a constraint.

> 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.
>

Huh?  While you are right that in both instances, the transform method 
or function needs to be called for each point calculation, a cylinder, 
an array light and a marble texture don't have the same parameters, so 
you _do_ have to rewrite the transformation code for each one anyway. 
To me it seems a lot more obvious to have an object call its internal 
transform() method while computing the color of a point on its surface, 
than to to have to externally figure out which transform function to 
call based on the type of object we're dealing with.

>>> 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...)

Huh.... Earlier you were complaining about having to specify public, 
private and const in Java.  _That_, along with garbage collection, is 
the safety they're talking about.

-- 
/*Francois Labreque*/#local a=x+y;#local b=x+a;#local c=a+b;#macro P(F//
/*    flabreque    */L)polygon{5,F,F+z,L+z,L,F pigment{rgb 9}}#end union
/*        @        */{P(0,a)P(a,b)P(b,c)P(2*a,2*b)P(2*b,b+c)P(b+c,<2,3>)
/*   gmail.com     */}camera{orthographic location<6,1.25,-6>look_at a }


Post a reply to this message

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