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:34:32 EDT (-0400)
  Re: Haskell vs Java: Building a ray tracer  
From: Orchid Win7 v1
Date: 28 Jun 2012 16:27:23
Message: <4fecbe2b@news.povray.org>
>> 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.

Valid point is accepted.

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

With a class, there's lots more code to write, and lots more to read. 
With this function, you're only writing the actual "functional parts", 
if you will. You don't have to perform a bunch of extra busy-work every 
time you want to do something.

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

Oh, no. I don't claim that any of my remarks are /original/. ;-)

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

And as I pointed out, this is not the case for many objects. E.g., a 
sphere has two points of intersection. These are the solutions to a 
quadratic equation. And we all know that the way to solve that is

   x = -b +/- Sqrt(b^2 - 4ac) / 2a

To find the "nearest" intersection, you merely pick the "-" rather than 
the "+". Done.

There are shapes where the procedure is not so simple. But that doesn't 
mean we can't have the benefit from the cases where it /is/ this simple.

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

Is it wrong that I actually know who those people are? o_O

>>>> Recall that in Java, ever single one of these things would be an entire
>>>> class.
>>>
>>> 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?

No. Because nodes need to change colour, and in Java [and most other OO 
languages] objects can't dynamically change class.

More to the point, red/black is just a boolean switch that controls 
which way a node is processed. Much like a priority on a job object, or 
whatever. However...

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

...the camera type completely changes the entire algorithm. Writing a 
giant switch{} statement to cover every possible camera type is

1. the exact problem that OOP was supposed to get rid of
2. prevents anybody adding new camera types

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

One of the fun things about Java is that it supports dynamic loading. 
You can throw a Jar file in a folder, and have the ray tracer load new 
code and immediately use it, without recompiling.

Or at least, you can if you /don't/ code your application using giant 
switch{} statements everywhere. :-P

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

Actually, yes you do.

Because Java is retarded, you're not allowed to inherit constructors. 
You can inherit regular methods, but not constructors. You have to 
explicitly write out the new constructor and make it explicitly call the 
old one, or it won't compile.

How's that for stupid?

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

It's the same thing as with cameras. If you implement each surface 
property as a separate class, then you can add more later. But if you 
implement one giant class that has every possible surface characteristic 
hard-coded into it, you can't do that any more. It's less modular in 
that way.

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

In the case of a ray tracer, you basically build a big static data 
structure (the scene), and then run some complicated algorithms over it 
(the ray tracer). On top of that, there are almost no special invariants 
which these data structures need to satisfy, meaning that there's almost 
no way to break the program. So issues like whether something is 
writable or not are pretty much moot in this specific instance. 
Obviously in other instances, it's rather important. Critical, even.

In Java, you can declare fields as constant, meaning that even if 
they're public, you can't alter them anyway. (But beware: If the field 
points to a mutable object, you /can/ still mutate that!) For the 
ray-tracer, you could equally declare everything private. You don't 
actually need field access at all, except within the object itself. As I 
say, in this example, it's all fairly moot either way.

Writing "public" or "private" isn't so bad I guess. But having to define 
all the fields, and then manually write a constructor with lists the 
same information /again/, and then manually assigns all the arguments to 
the fields... that's just tedious. Again, in some other situation, you 
might want your constructor to actually perform some real initialisation 
work. But in this specific case, there's nothing to initialise.

Haskell doesn't make me write initialisers. If I want one, I can write a 
special initialisation function and then do some mojo to prevent people 
forgetting to use it. And if I don't need that, I can just leave it. 
Either way, I'm not manually writing useless boilerplate code.

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

I will try again. Suppose for argument's sake we want to rotate an 
object through 13 degrees. You can do this two ways:

- Tell the object "rotate yourself 13 degrees", and have the object 
update all its coordinates and so forth. Of course, every type of object 
is defined in a different way. For a sphere, you'd rotate just the 
center coordinates. For a cube, you'd rotate two corner coordinates. For 
a polygon, you'd rotate each corner coordinate. And so forth.

- Wrap the object in a transform object, and replace the original object 
with the wrapped one. Now every time you say "what intersections do you 
have with this ray?", the wrapper object transforms the ray, and then 
asks the original object "what intersections do you have with this 
transformed ray?" The original object has no idea that anything has 
changed. You can implement the wrapper object once, and use it to 
transform any possible object.

On the downside, the first way involves transforming the object once, 
whereas the second involves transforming every individual ray...

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

Java is safer than C, that I'll grant you. But that's not the same as 
"safe".

Every time a Java program has an int field and a bunch of public static 
final int FOO = 0x384 declarations, you just /know/ that somebody, 
somewhere is going to put an invalid value into that field. And then the 
programmer must explicitly write a manual runtime check for this 
condition, and decide what to do about it if it occurs, and test that 
this works.

A Haskell programmer can simply write

   data Mode = Foo | Bar | Baz

and rest safe in the knowledge that the Mode field will /always/ be one 
of the tree valid possibilities. No need to write code to check this, no 
need to write test cases to check for it. No need for the client to 
guess what happens if this isn't true. Much. Safer.

Java inherits C's strange conventions of returning an int value for 
whatever, unless there's in error, in which case it returns a negative 
integer. And if you forget to test whether the result value is negative, 
you now have a runtime bug.

(Exhibit A: Reader.read() returns int, not char, because that way it can 
return a character code unless EOF is reached in which case it returns 
-1. WTF?)

Haskell has Maybe. If an operation can fail, you return Maybe Int 
instead of just Int. The API tells you "this operation can fail". You 
can't forget this fact. If you don't check for failure, your code does 
not compile. (Stick /that/ in your pipe and smoke it, Mr 
Checked-Exceptions-Unless-They-Subclass-java.lang.Error :-P ) You can't 
just "ignore" failure; you have to actually define what to do about it. 
(Although you can sort-of get away with returning a plausible but wrong 
default value if you're hell-bent on writing bad code.)

C behaves in undefined ways on a null pointer. Java throws an exception 
on a null pointer. Haskell doesn't /have/ pointers, so they can never be 
null. :-P

I could go on at length... but I won't. (Mainly because I know nobody 
except me cares.)


Post a reply to this message

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