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:24:32 EDT (-0400)
  Re: Haskell vs Java: Building a ray tracer  
From: Francois Labreque
Date: 28 Jun 2012 12:34:16
Message: <4fec8788@news.povray.org>


[SNIP Tolstoi novell about java]

> 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 fonction handles this Vector3 
object?

> Note, also, that we don't need to do this stupid
>
> public Vector3(double x, double y, double z) {X = x; Y = y; Z = z;}
>
> and then write
>
> new Vector3(1, 2, 3)
>
> every time we want a vector. Instead, you can simply do
>
> Vector3 1 2 3
>
> to create a vector.

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

Then,

new Vector3(1, 2, 3)

vs.

Vector3 1 2 3

Is only a matter of language syntax.  You save 7 characters.  Whooptidoo!

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

> These, of course, are mere trifles. Let us get to the real meat of the
> problem. Now, we need to implement shapes somehow. The /obvious/ thing
> to do is copy the Java version; define a class for shapes, and then
> create various data structures representing concrete shapes:
>
> class Shape s where
> isect :: s -> Ray -> [Double]
> normal :: s -> Vector3 -> Vector3
> inside :: s -> Vector3 -> Bool
>
> data Sphere = Sphere {center :: Vector3, radius :: Double}
>
> instance Shape Sphere where
> isect (Ray s d) (Sphere c r) = ...
>
> data Plane = Plane {normal :: Vector3, offset :: Double}
>
> instance Shape Plane where
> isect (Ray s d) (Plane n o) = ...
>
> Notice the isect function. In Java, we have /three/ functions for
> efficiency; one function only bothers to compute /whether/ there are any
> intersections, another that only bothers to compute the /first/
> intersection, and another which computes /all/ intersections.
>
> 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. And obviously, accessing only the
> first solution does not cause any further solutions to be computed. Note
> we're actually doing /better/ than Java: If our CSG happens, at
> run-time, to need the first /three/ intersections but no further, then
> only these are computed.
>
> (In fairness, we could use an Iterator in Java to achieve the self-same
> thing. The hasNext() function would enable intersection tests without
> computing the actual location, and next() would compute one additional
> intersection. It would be really quite a lot of work to set up this much
> lazy evaluation manually, though. Haskell gives it to us for free!)
>

You already do it for AllIsect().  There's no additional work necessary.

Also, bug report time:  AllIsect() returns an array of doubles, yet you 
don't specify the size of the array in question.  How would the caller 
know how the size of the array?  (Disclaimer: I'm not very fluent in 
Java, so if there's some embedded mechanism to prevent your from falling 
off the edge of an array, nevermind)

[more snippage]

> Similarly, a Texture becomes utterly trivial:
>
> type Texture = Vector3 -> Surface
>
> In general, and 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

> data RayIsect =
> RayIsect
> {
> isect_point :: Vector3,
> isect_direction :: Vector3,
> isect_normal :: Vector3
> }
>
> type Surface = Scene -> RayIsect -> Colour
>
> Notice, again, that we can fill out /all/ fields, and then lazy
> evaluation will skip any calculations we don't need. In particular,
> Surface types that don't depend on the surface normal can skip the
> surface normal calculation.
>
> With the definitions above, a "constant texture" becomes so trivial it's
> barely worth defining a name for it. You can just write "const s" and
> you're done. Let's look at some simple instances:
>
> cam_orthographic :: Vector3 -> Vector3 -> Vector3 -> Vector3 -> Camera
> cam_orthographic v0 vx vy vz =
> \ (Vector2 x y) = Ray {start = v0 + x *| vx + y *| vy, direction = vz}
>
> cam_perspective :: Vector3 -> Vector3 -> Vector3 -> Camera
> cam_perspective vx vy vz =
> \ (Vector2 x y) = Ray {start = v0, direction = vnormalise (x *| vx + y
> *| vy + vz)}

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

>
> sur_ambient :: Colour -> Surface
> sur_ambient c = \ _ _ -> c
>
> sur_diffuse :: Colour -> Surface
> sur_diffuse c =
> \ scene isect ->
> let
> fn light =
> if shadow_test scene light isect
> then cBlack
> else c *| (light_point light `vdot` isect_point isect)
> colours = map fn (scene_lights scene)
> in sum colours
>
> sur_reflect :: Colour -> Surface
> sur_reflect c =
> \ scene isect ->
> let
> v_in = isect_direction isect
> v_norm = isect_normal isect
> v_out = v_in - 2 * (v_in `vdot` v_norm) *| v_norm
> in trace_ray scene (Ray {start = isect_point isect, direction = v_out})
>
> 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, or two dereived classes that 
inherit from a generic camera class, if you prefer.

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.

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

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

>
> 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? Compare Java:
>
> public class TransformTexture extends Texture
> {
> public final Transform Trans;
> public final Texture Text;
>
> public TransformTexture(Transform t1, Texture t2)
> {Trans = t1; Text = t2;}
>
> public Surface GetSurface(Vector3 p)
> {
> return Text.GetSurface(Trans.Apply(p));
> }
> }
>
> What a load of waffle! Especially when you consider that a typical
> Haskeller would probably write
>
> transform_texture :: Transform -> Texture -> Texture
> transform_texture = flip (.)
>
> and be done with it!
>
> (This works because this convoluted-sounding concept of "apply a
> coordinate transformation to the input before passing it to the texture"
> is really nothing other than /function composition/, which is a
> well-known concept in Haskell. A coordinate transformation maps a point
> to a point, a texture maps a point to a Surface. Combining these steps
> is clearly function composition. When you think about it like that...
> suddenly it's /obvious/ that this is a trivial thing.)
>
> When you start thinking of (say) a coordinate transformation as a
> trivial function rather than as some complex active "object" which has
> "behaviour", "state" and "identity" which needs "constructors" and
> "destructors" and "gettters" and "setters" and "reflection" and... Well,
> suddenly all your problems start to look a hell of a lot simpler.
>
> Take a look back at all those "map" classes I had. In Haskell, each of
> these is merely a function. It's hardly worth assigning names to them.
> Indeed, in Java I had trouble picking suitably descriptive names for all
> the combinations and variations I could come up with. In Haskell, I
> don't need to bother. If I want to deal with functions that map points
> to scalars, I can just /call/ them "functions that map points to
> scalars". Which is actually less confusing then naming them ScalarMap or
> whatever. It more succinctly conveys what they do.
>

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.

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

I remember a discussion a long, long time ago, in a compl.lang.lisp far, 
far away of number-crunching speed comparisons (IIRC, positions of the 
planets around the sun for a command-line spcified date) between Lisp 
and C, where 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.

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