Building code. A multi-language comparison.
A COMPARISON OF CODE IMPLEMENTATION IN 5 DIFFERENT PROGRAMMING LANGUAGES
BACKGROUND
I have been working on a book project where the idea is to create a collection of primers that follow the same?teaching pattern. Each primer focuses on a specific language, walking the reader through making a program that can plot basic fractals.
The core assumption behind the books, is that programming languages are so similar that we should be able to split the introductory text in two:
Using pseudo-code to illustrate concepts, is not exactly a new thing, but how far will it take us, with regards to creating a generic introduction spanning a trove of languages? I have used several programming languages over the years, but I have never actually tried to compare them. There is a conceptual assumption behind teaching a multitude of languages from one source, that need to be proven. Or rather, I need to uncover any major stumbling blocks, under the assumption that "all" languages are similar (enough).
THE ALGORITHM
It is quite easy to draw 2D color plots in code.? We simply traverse every pixel on the screen while tracking each pixel's coordinate. Starting at one corner, we process one pixel at a time, one line at a time.
For each coordinate, we feed the values into a system that transforms them to a color value. We then plot that color at the current screen position:
We are free to implement whatever transformational system we want, but in this case, we use the pixel coordinates to determine a point in the Mandelbrot set. Or rather, the corresponding color value for the point within the set.
GENERIC IMPLEMENTATION
When writing, what is supposed to be a language agnostic implementation, it is important that we use mechanisms that aren't?prone to language-specific shenanigans. Specifically, when it comes to plotting to screen, what could be a simple plot function in one language, could easily be more elaborate in another. Aiming to take control of that, I decided to go for a sort of bare-metal approach:
While this may seem slightly complicated at first, it allows us to abstract away language and environment details.
It allows us to only focus on our 2D table of data, when working with graphics. We then let the programming language worry about the rest.
SPECIFIC IMPLEMENTATION
Depending on the language, there will be some boiler-plate code. Both in terms of getting to the "Hello World" stage, but also in terms of displaying graphics.
However, since we are looking to compare languages in terms of user implementation, we can safely ignore-boiler plate for now.
Our implementation ends up with three parts:
main(): Every program has a starting point. Here, I use main to denote this part of the code. It sets up the environment and eventually calls upon draw() to provide the color data. It then uses the color data to paint the screen surface accordingly.
draw(): This is sort of the "boring" part of the color drawing code. It traverses the screen while keeping track of the pixel coordinates.
For every pixel, it calls upon getColor(), to figure out what color to use when painting a pixel. Or rather, what color to store in our color buffer.
getColor(): Finally, we get to the juicy part. This is where we decide how to apply colors. We could make it return a static color, in which case the output would be a single-colored surface. We could apply some fancy formulas to create nice patterns, or as in our case: we can calculate a color associated with the Mandelbrot set.
LANGUAGE IMPLEMENTATION
The code to generate this image, was built and tested in:
There are more languages in the pipeline, but this should provide a good start. For now, let's focus on and compare the two functions: draw() and getColor() respectively.
IMPLEMENTATION OF THE draw() FUNCTION
The draw() function is all about traversing the screen. We need to run through every pixel, for every line. This translates nicely into a couple of nested for-loops.
C# ( using WPF for screen access ) :
void draw(byte[] buffer, int width, int height )
{
? ? int maxPixelIndex = width - 1;
? ? int maxLineIndex = height - 1;
? ? int index = 0;
? ? for (int y = 0; y <= maxLineIndex; y++)
? ? {
? ? ? ? for (int x = 0; x <= maxPixelIndex; x++)
? ? ? ? {
? ? ? ? ? ? (byte r, byte g, byte b) color = getColor(x, y);
? ? ? ? ? ? buffer[index++] = color.r; // Red component
? ? ? ? ? ? buffer[index++] = color.g; // Green component
? ? ? ? ? ? buffer[index++] = color.b; // Blue component
? ? ? ? ? ? buffer[index++] = 255; // Alpha component
? ? ? ? }
? ? }
}
The draw function takes a color buffer ( byte[] ) as input along with the buffer dimensions, calculate and store the corresponding color value.
The color buffer in this case is segmented into bytes. Each color has four components ( Red,Green,Blue and Alpha). The alpha component controls the opacity. At 255 the color is fully opaque.
领英推荐
C++ (using SDL2 for screen access):
void draw(uint32_t* buffer, SDL_Texture* texBuffer, int width, int height)
{
int maxPixelIndex = width - 1;
int maxLineIndex = height - 1;
int index = 0;
for (int y = 0; y <= maxLineIndex; y++)
{
for (int x = 0; x <= maxPixelIndex; x++)
{
unsigned int* color = getColor(x, y);
unsigned int r = color[0]; // Red component
unsigned int g = color[1]; // Green component
unsigned int b = color[2]; // Blue component
buffer[index++] = 0xFF |(r << 24)|(g << 16)|(b << 8);
}
}
SDL_UpdateTexture(texBuffer, NULL, buffer, width * sizeof(uint32_t));
}
The C++ code and the C# code are similar. The difference stems from the how the underlying graphics library arranged the buffer. In this case, the color buffer is segmented into 32bit values and each position in the buffer corresponds to 4 bytes. In other words, each position corresponds to a color definition ( R,G,B and A) .
Modern C++ allows for working with features such as Tuples. We could have used them to make the C++ code look more or less identical to the C# code, but I decided to avoid using any extra libraries unless I absolutely had to. In this case arrays and pointers worked just fine.
Although, it's worth noting that the pointer-born arithmetic makes it slightly harder to read.
Python (with numpy & pil for screen access):
def draw(buffer, width, height)
? ? ? ? maxPixelIndex = width - 1
? ? ? ? maxLineIndex = height - 1
? ? ? ? for y in range(maxLineIndex):
? ? ? ? ? ? for x in range(maxPixelIndex):
? ? ? ? ? ? ? ? buffer[y][x] = getColor(x,y) ? ? ? ? ? ? ? ?
? ? ? ? return
The python version is short and sweet. Using indentation to segment code-blocks, keeps the code very compact. We could probably have improved the C++ version, by adding in a separate helper function to deal with buffer storage. With Python, there is no need for that.
GoLang ( using Fyne V2 for screen access):
drawScreen := func(width int, height int) {
? ? maxPixelIndex := width - 1
? ? maxLineIndex := height - 1
? ? for y := 0; y <= maxLineIndex; y++ {
? ? ? ? for x := 0; x <= maxPixelIndex; x++ {
? ? ? ? ? ? poscolor := getColor(x, y)
? ? ? ? ? ? rgbaImage.Set(x, y, poscolor)
? ? ? ? }
? ? }
}
The GoLang implementation is very similar to the Python implementation. Again, the difference stems from the underlying graphics library (Fyne), where we have to set the color value through a function, rather than assigning it to a position in the buffer directly, as we did in the Python code.
JavaScript (using canvas context for screen access):
function draw(buffer, width,height)
{
? ? let maxPixelIndex = width - 1;
? ? let maxLineIndex = height -1;
? ? let index = 0;
? ? for(let y=0;y<=maxLineIndex;y++)
? ? {
? ? ? ? for(let x=0;x<=maxPixelIndex;x++)
? ? ? ? {
? ? ? ? ? ? let color = getColor(x,y);
? ? ? ? ? ? buffer[ index++ ] = color[0]; // Red component
? ? ? ? ? ? buffer[ index++ ] = color[1]; // Green component
? ? ? ? ? ? buffer[ index++ ] = color[2]; // Blue component
? ? ? ? ? ? buffer[ index++ ] = 255; // Alpha component
? ? ? ? }
? ?}
}
Finally, we have the JavaScript implementation. The code is almost identical to that of the C# implementation. The only difference is in how we access the color component data.
IMPLEMENTATION OF THE getColor() FUNCTION
Let us have a look at getColor() to see what code to generate Mandelbrot data points would look like in the selected languages.
C# (using WPF for screen access) :
(byte r, byte g, byte b) getColor(int x, int y )
{
//COLOR SCHEME
(byte r, byte g, byte b) settledColor = ( 30, 30, 255 ); // Light blue?
(byte r, byte g, byte b) explodedColor = ( 0, 0, 0 );? ? // Black
//PARAMETERS
int maxLoopCount = 256;
double threshold = 4.0;
//INITIALIZE LOOP
double a = 0.0, b = 0.0;
double Ca = (double)(x * 0.0032 - 2.9); // Hor. pos in Mandelbrot space
double Cb = (double)(y * 0.0032 - 1.95);? // Vert pos in Mandelbrot space
int i; bool exploded = false;
for (i = 0; i < maxLoopCount; i = i + 1)
{
? ? //UPDATE STATE
? ? double a_new = a * a - b * b + Ca;
? ? double b_new = 2 * a * b + Cb;
? ? a = a_new; b = b_new;
? ? if (Math.Sqrt(a * a + b * b) >= threshold) { exploded = true; break; }
}
// RETURN APPROPRIATE COLOR
if (exploded) { return explodedColor; } else { return settledColor; }
}
The Mandelbrot calculation keeps re-iterating a feed-back loop. The values it outputs (a_new and b_new), are put back into the equations. We then check whether the system settles or takes off to infinity. Ideally it should keep looping forever, but as with most things, we settle for good enough. We do this by limiting the number of loops with maxLoopCount.
C++ (using SDL2 for screen access):
unsigned int* getColor(int x, int y)
{
// COLOR SCHEME
unsigned int settledColor[3] = {30,30,255}; // Light blue?
unsigned int explodedColor[3] = { 0,0,0 }; // Black
//PARAMETERS
int maxLoopCount = 256;
double threshold = 4.0;
//INITIALIZE LOOP
double a = 0.0, b = 0.0;
double Ca = (double)(x * 0.0032-2.9); // Hor. pos in Mandelbrot space
double Cb = (double)(y * 0.0032-1.95);? // Vert pos in Mandelbrot space
int i; bool exploded = false;
for (i = 0; i < maxLoopCount; i = i + 1)
{
// UPDATE STATE
double a_new = a * a - b * b + Ca;
double b_new = 2 * a * b + Cb;
a = a_new; b = b_new;
if (sqrt(a * a + b * b) >= threshold) { exploded = true; break; }
}
// RETURN APPROPRIATE COLOR
if (exploded) { return explodedColor; } else { return settledColor; }
}
Other than how colors are represented; the C++ code and the C# code are virtually identical.
Python:
def getColor(x,y)
? ? ## COLOR SCHEME
? ? explodedColor = [30,30,255]
? ? settledColor = [0,0,0]
? ? ## PARAMETERS
? ? maxLoopCount = 256
? ? threshold = 4.0
? ? ## INITIALIZE LOOP
? ? a = 0; b = 0
? ? Ca = x*0.0032-1.9 ?# Hor. pos in Mandelbrot space
? ? Cb = y*0.0032-0.9 ?# Vert pos in Mandelbrot space
? ? exploded = False
? ? for i in range(maxLoopCount):
? ? ? ? ## UPDATE STATE
? ? ? ? a_new = a*a - b*b + Ca
? ? ? ? b_new = 2*a*b + Cb
? ? ? ? a = a_new
? ? ? ? b = b_new
? ? ? ? if(mth.sqrt( a*a + b*b )>= threshold):
? ? ? ? ? ? exploded = True
? ? ? ? ? ? break
? ? ## RETURN APPROPRIATE COLOR
? ? if(exploded):
? ? ? ? return explodedColor
? ? else:
? ? ? ? return settledColor
The code is very similar to the C++/C# code. The major difference is that we do not need to declare the data types.
GoLang:
getColor := func(x int, y int) color.RGBA
? ? ? ? // COLOR SCHEME
? ? ? ? settledColor := color.RGBA{30, 30, 255, 0xff}
? ? ? ? explodedColor := color.RGBA{0, 0, 0, 0xff}
? ? ? ? // PARAMETERS
? ? ? ? maxLoopCount := 256
? ? ? ? threshold := 4.0
? ? ? ? // INITIALIZE LOOP
? ? ? ? xx := float64(x); yy := float64(y) // Cast from int to float
? ? ? ? a := 0.0; b := 0.0
? ? ? ? Ca := xx * 0.0032 - 1.9
? ? ? ? Cb := yy * 0.0032 - 0.95
? ? ? ? exploded := false
? ? ? ? for i := 0; i < maxLoopCount; i++ {
?
? ? ? ? ? // UPDATE STATE
? ? ? ? ? ? a_new := a*a - b*b + Ca
? ? ? ? ? ? b_new := 2*a*b + Cb
? ? ? ? ? ? a = a_new
? ? ? ? ? ? b = b_new
? ? ? ? ? ? if math.Sqrt(a*a + b*b) >= threshold {
? ? ? ? ? ? ? ? exploded = true
? ? ? ? ? ? ? ? break
? ? ? ? ? ? }
? ? ? ? }
?
## RETURN APPROPRIATE COLOR
? ? ? if exploded {
? ? ? ? ? ? return explodedColor
? ? ? ?} else {
? ? ? ? ? ? return settledColor
? ? ? ?}
}
Again, the code is almost exactly the same. The only difference, beyond language flair, is that we had to cast the position variables (x and y) into variables of type float64.
Javascript:
function getColor(x,y)
{
? ? // COLOR SCHEME
? ? let explodedColor = [255,30,255];
? ? let settledColor = [0,0,0];
? ? // PARAMETERS
? ? let maxLoopCount = 256;
? ? let threshold = 4.0;
? ? // INITIALIZE LOOP
? ? let a = 0.0, b=0.0;
? ? let Ca = x*0.0032-2.9; ?// Hor. pos in Mandelbrot space
? ? let Cb = y*0.0032-1.95; // Vert pos in Mandelbrot space
? ?
? ? let i; let exploded = false;
? ? for(i=0;i<maxLoopCount;i++)
? ? {
? ? ? ? // UPDATE STATE
? ? ? ? let a_new = a*a - b*b + Ca;
? ? ? ? let b_new = 2*a*b + Cb;
? ? ? ? a = a_new; b = b_new;
? ? ? ? if( Math.sqrt(a*a+b*b)>= threshold) {exploded = true; break;}
? ? }
? ? // RETURN APPROPRIATE COLOR
? ? if(exploded){ return explodedColor;} else {return settledColor;}
}
The implementation is almost exactly the same as that for C#. The only difference lies in language specifics with respect to declaring variables.
CONCLUSION
It doesn't come as a huge surprise that the solutions are similar, but it is still interesting to see exactly how similar they are when seen up close.
Learning the whole eco-system of a language and becoming proficient in any given language will always be a huge undertaking. However, in terms of reading code, it seems a seasoned programmer should be able to hop from one language to another with ease.
This of course stems from the fact that language implementations, in its basic form, address mostly the same programming concepts. Loops, functions, numerical operations etc. They work much the same; it is just the dressing that is slightly different.
My original presumption, was that many languages are so similar that I should be able to apply the same generic introduction for all of them, and then simply extend on that to address a specific language.
That does indeed seem to be a valid supposition. If not for all languages, it seems to be true for quite a bit of them; certainly, enough to justify the effort of bringing about a series of programming primers.
Wish me luck!