Beyond "Hello World" - comparing starter code in C, Rust, Python, TypeScript and more
Tai Kedzierski
Posted on August 15, 2024
Cover image (C) Tai Kedzierski
TLDR : The classic "Hello World" doesn't show enough of a language to get a flavour for it - in this post we explore the same example written in several languages for comparison. "Hello Y'All" is a simple example that exercises more of a language. How does the "Hello Y'All" example work in your favorite languages?
Disclaimer: none of the code here is intended to be "idiomatic" or "optimal". I'm a learner especially of the systems languages myself. Any suggestions for correction and improvement are welcome, and possibly even applied if compelling.
The examples in this article have been posted at https://github.com/taikedz/hello-yall . A yet-to-be-done "advanced example" is in the works as well
- Hello Y'All basics
- Python
- Bash
- TypeScript via Bun
- PHP for Web
- C
- Go
- Zig
- Rust
- Lua
- Java
- Groovy
- Perl
- Extended Bash for Bash Builder
- Outro
"Hello World" is the classic example program every new user is exhorted to write: superficially it serves as a way of showing how easy it is to do a basic task in the language; but it also serves as a simple way to check that the base tools are installed.
For example, just making sure python is installed properly, one can write the following:
# some pythonfile like "spam.py"
print("Hello World!")
And just run it:
python spam.py
As a demonstration of how easy it is to get started, it contrasts with the likes of Java quite directly:
// MUST be "HelloWorld.java" file - public class name must match the file name
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hellow World!");
}
}
Then to compile and run it:
javac HelloWorld.java
# Produces a HelloWorld.class
# To run it though, don't include ".class" !
java HelloWorld
Arguably, it's a good tooling check, but it says little about how easy it is to do some basic things in a language.
The one thing that always costs me time in getting started with any language is navigating its intricacies on splicing functions out to other files locally - and then importing them. Another is just wanting to see a simple example of how some basic flow works in the language: calling a function, returning, concatenating strings, and a simple iteration.
Hello Y'All
I'd like to propose the following as a good reference example to showcase basic functionality in a single page - one that should be a very basic challenge, but with enough in it to allow a seasoned developer to see the basic features, and appreciate the tutorials properly from there.
So here's a "Hello Y'All" program to implement along the following rules:
- The example takes at least one command line argument, and handles the event of no-arguments
- The example requires a function to be placed in a second file in a subfolder
- The function returns a value
- The example loops over CLI arguments, calling one at a time to the function, and each time prints the returned value, concatenated to other text
- Include the overall bare-minimum instructions include how to build the program (if necessary) and run it
Why these requirements ? They showcase some of the basic features every language should support, for one; but also if you've already got a couple of languages under your belt, it is helpful to get a more holistic sense of the language's design and approach in one go, in a way that allows consistent comparison across languages.
We will see over the next few examples that splitting code into local files is not always intuitive - every language has its way of handling importing, according to the architectural/environmental constraints of its time and/or target use-case. This is the main reason I was compelled to lay out a base example principle and write this post. Both in Rust and Go, I found myself mired in a hell of answers on "how to import external packages" and needless hunting for the answer to "how to import a local file."
Python
Python makes importing local files very simple, both syntactically and operationally - part of the "batteries included" approach. For sanity, I recommend PYTHNOPATH
point to the directory containing the top level folder, always; it is usually optional as python does try to resolve things automatically, but sometimes it just needs the PYTHONPATH
. I only learned recently that __init__.py
files were optional ...
Create a ./hey.py
file with these contents:
import sys
import stuff.hello as hello
if len(sys.argv) < 2:
print("No arguments specified!")
for name in sys.argv[1:]:
phrase = hello.greet(name)
print( phrase )
Create a ./stuff/hello.py
file with these contents:
def greet(name):
return f"Hello {name}"
To run it:
# Good practice to ensure python knows where to resolve top-level imports from
export PYTHONPATH=.
# Run it
python hey.py Jay
Bash
Bash is a language that few really take the time to learn properly. Lots of unwieldy and unmaintainable bash scripts get written, which is a shame since... it typically is also code that holds up your business. Give it more love. ❤️
In ./hey.sh
put this:
# Imports the file contents ; all tokens in the file are added to the global scope
source stuff/hello.sh
if [[ -z "$*" ]]; then
echo "No arguments supplied"
exit 1
fi
for name in "$@"; do
phrase="$(hello_greet "$name")"
echo "$phrase"
done
In ./stuff/hello.sh
write:
hello_greet() {
# Arguments to functions are positional
echo "Hello $1"
}
Run it:
bash hey.sh Jay
Note that source
-ing is specifically relative to the current working directory! Source-ing from an already-sourced file is unreliable at best. For this reason, there is not much of a habit of splitting code into files where bash scripts are involved, and was the impetus behind writing bash-builder
TypeScript via Bun
I don't think I could get away with not including TypeScript on this list (I am forgoing trying to deal with JavaScript). And I certainly don't want to deal with discussing NodeJS and npm.
Bun makes dealing with the JavaScript ecosystem easier, and removes the need for a lot of the independently existing standard tools. node
/nodejs
and npm
commands can be replaced by bun
as a command, and conflicting requirement dependencies don't crop up. As a newcomer to it, do yourself a favour.
Write in ./hey.ts
:
import { greet } from './stuff/hello.ts'
if (process.argv.length <= 2 ) {
console.log("Please supply names !");
process.exit(1);
}
for(var name of process.argv.slice(2)) {
var phrase = greet(name);
console.log(phrase);
}
And in ./stuff/hello.ts
:
export function greet(name:string) {
return `Hello, ${name}!`;
}
Run this through bun:
bun hey.ts Alex Sam Jay Pat
If you need to compile it down to plain JavaScript:
bun build hey.ts > hey.js
Just be warned that in JavaScript and TypeScript, [7,-2,6,-5].sort() == [ -2, -5, 6, 7 ]
is a reality. It's not the only madness still around in 2024 in the language. This should have been specified out by now, surely...
Web Server PHP
PHP has received much hate over the years, but let's not forget that we wouldn't have the Web of today without it (MediaWiki, WordPress and facebook are the notable projects that got a head start with it), and free tier hosting. You can be up and running with a website and even a working proof of concept business using PHP and a free webhosting tier faster than with any other language - instead of chaining yourself to excessively "flexible" billing.
This example takes a slightly different form, assuming the base case is in fact to run as a server script - so we'll retrieve from the GET request's query map, rather than iterating a list.
In ./hey.php
write:
<?php
require_once "stuff/hello.php";
if(count($_GET) == 0) {
echo "Give me a query!";
} else {
foreach ($_GET as $name => $place) {
$phrase = hello_greet($place, $name);
echo "<p>$phrase</p>\n";
}
}
?>
In ./stuff/hello.php
write:
<?php
function hello_greet($origin, $name) {
return "Hello $name from $origin !";
}
?>
Upload these to your web server hosting root (usually a www/
folder), and visit your site. Assuming it's served at http://hosting.tld/~you
then you would go to http://hosting.tld/~you/hey.php?Kim=Korea,Sam=Sweden
This will simply write the greetings in the web page.
C
Ah C. The gold standard of programming, besieged on all sides by upstarts. Zig, Rust, Go, all vying to be "the next C". And for understandable reason - so many catastrophic bugs and security issues stem from the mismanaged memory and ill-conceived pointer arithmetic.
But, it is still a much needed and much beloved language, and essential in embedded programming. Perhaps Zig or Rust will replace it there, one day. But what is sure is that it is not leaving the party any time soon.
Write a ./hey.c
#include <stdlib.h>
#include <stdio.h>
#include "stuff/hello.h"
int main(int argc, char* argv[]) {
if(argc < 2) {
// There is always at least one arg at argv[0], the program's name
printf("No arguments supplied\n");
return 1;
}
for(int i=1; i < argc; i++) {
char* phrase = hello_greet(argv[i]);
printf("%s\n", phrase);
// For completeness, we note here that any allocated memory
// MUST be deallocated - and ONCE only !
free(phrase);
}
}
Then write a ./stuff/hello.c
#include <stdlib.h>
#include <string.h>
char* hello_greet(char* name) {
// Constant, on the stack
char* base_hello = "Hello ";
// Because we're returning the data out of a function,
// we need it on the heap
// The +1 is for the null-terminator on the end of the string
char* phrase = malloc(strlen(base_hello) + strlen(name) + 1);
strcpy(phrase, base_hello);
strcat(phrase, name);
return phrase;
}
C requires using header files .h
- one doesn't import source files directly, but instead you compile all the source files as binary, import the headers as a way of resolving symbols. I don't have the ability to discuss the itnernals of this from a compiler perspecitve, but it is done this way for consistency : if you are including a pre-compiled library from elsewhere, you need a header file to tell you and the compiler what's actually available. I think.
Write stuff/hello.h
// We guard against multi-inclusion by setting a one-time declared name
#ifndef HELLO_H_
#define HELLO_H_
// This tells the compiler that it should expect to find such a
// function defined in the binary code that is being linked
char* hello_greet(char* name);
#endif // HELLO_H_
Run it:
# See that we _compile_ the source files
# even though we are '#include'-ing header files
gcc -o hey hey.c stuff/hello.c
./hey Jay
See the pretenders to the throne below ...
Go
I've seen Go be ascribed a number of lofty goals - a better C (for general cases yes, but it won't take embedded and kernel spaces with it, and I'm not yet sure about its interop capabilities), a replacement for Python (it's more performant, but much less friendly syntactically - for better or worse), and a replacement for shell scripting (I can kinda see that, if I squint hard enough).
All imports are done from paths along the GOPATH
(and from the current project as discussed below) which usually points to a single directory, usually in ~/.go
- not entirely sure why that is, as different projects will usually have different dependency needs - but that's the convention. You can of course change the GOPATH
as required, but that is neither necessary nor in scope for this example - it should be fully irrelevant. If the compiler spits out something about not finding something on the GOPATH, the example (or your copying of it) should be suspected first...
Your project needs to be initialised - in the top level of the project, do go mod init $module_name
, for example
go mod init net.myname.greeter
... conventionally using a reverse domain notation.
The simple way of splitting code into several files is to simply write several *.go
files, all with package main
declarations, and running go build -o my-app src/*.go
. All symbols from these files will be directly available in the scope of any main
-package file. As a build requirement, all files must be in the same directory. In this mode, it's not possible to store some files into subfolders.
To get past the same-dir restriction and be able to place our greeter in its own folder, we need to create a submodule - specifically we are creating an explicit module under a different package
declaration.
In ./src/hey.go
write
package main
import (
"fmt"
"os"
"log"
// This is the _path_ to the package
"net.myname.greeter/src/stuff"
)
func main() {
if(len(os.Args) < 2) {
log.Fatal("Please supply arguments!")
}
for _, person := range os.Args[1:] {
// the _name_ of the imported package is "stuffy"
var phrase string = stuffy.Greet(person)
fmt.Println(phrase)
}
}
The module is created in a subfolder, and the main file must share the name of its parent folder.
In src/stuff/stuff.go
write
// Package actual name, regardless of the folder/file name
package stuffy
// Because this function is to be used from outside the `stuff` package
// it needs to be Capitalised.
func Greet(name string) string {
return "Hello " + name
}
Build and run:
go build src/hey.go
./hey Alex Sam
# To just run without storing a build file:
go run src/hey.go Alex Sam
To add more files and functions to stuff/
, simply add the ./stuff/*.go
files, all with package stuff
declarations. All files with the same package stuff
declaration will be able to use eachothers' symbols within the same stuff
package.
Zig
From the little experimentation I've been doing, Zig seems to be the most likely contender in the race to replace C. I've not yet properly sat down to do a mini-project, so this Hello Y'All example is going to be the most complete amount of active work I'll have done...
The whole "allocators" pattern takes getting used to and was arguably the item I had to spend most time on. There are several types of allocators (many more than in that article), and choosing one is no doubt an expression of sagacity. No doubt the neophyte would best be served by using the General Purpose Allocator as they start out. There's a few patterns, most generically the alloc
/free
pattern. If creating memory for structs, it's var thing = allocator.create(your_struct) ; defer allocator.destroy(thing)
. If it's an arena allocator for short-lived one-run programs, arena_alloc.init()
the item, and then defer arena_alloc.destroy()
the whole thing at the end of the progam.
I do not claim to have grasped the subtleties at this point. The following code may yet be suboptimal. On the plus side... no header files !
Write one file hey.zig
:
const std = @import("std");
const hello = @import("stuff/hello.zig");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// We need to manage the allocator in the very space we are going to call its free/destroy from
// we can't confine it easily to another function, so it feels a little cluttered ...
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allo = gpa.allocator();
defer _ = gpa.deinit();
const args = try std.process.argsAlloc(allo);
defer std.process.argsFree(allo, args);
if(args.len <= 1) {
try stdout.print("Please specify arguments!\n", .{});
std.process.exit(1);
}
var phrase:[]u8 = undefined;
for(args[1..]) |name| {
// Because hello.greet creates the data and chucks it back out, we pass in
// the allocator, so that we can retain responsibility for freeing
phrase = try hello.greet(name, allo);
defer allo.free(phrase);
try stdout.print("{s}", .{phrase});
}
}
And then write ./stuff/hello.zig
const std = @import("std");
pub fn greet(name:[]const u8, allo:std.mem.Allocator) std.fmt.AllocPrintError![]u8 {
// Pass-down an allocator. All standard library functions which need
// heap space will take an allocator, instead of using their own
const phrase = try std.fmt.allocPrint(
allo,
"Hello, {s}!\n",
.{name}
);
return phrase;
}
Then compile and run:
# Compile
zig build-exe hey.zig
# Run
./hey
./hey Sam Pat
Rust
Rust's documentation is a bit light on the explanations of splitting code out into folders, but I got there eventually - not sure I remember how, so I cannot offer any links.
Rust is very fussy when it comes to types, and probably one of the biggest gripes I have with it is its myriad String
/&str
/OsString
/&osstr
and goodness knows what else types. A lot of the complexity in learning Rust comes from thinking about ownership and lifetimes which are made explicit to prevent memory issues.
Largely, it feels like the comment on Lisp: learn it, and it will make you a better programmer, but you probably won't want to keep it as a main language. Not to say that there aren't Rust devs (goodness knows there are quite a few now), but for the common mortal who is not systems-programming, it is more than what is needed to get a job done.
I have included quite a bit of commentary because I feel the need to explain why the example isn't simpler than it is...
For the top level file, write in ./src/hey.rs
// Declare that there is a module "stuff" ...
mod stuff;
use std::env;
// Use the "hello" submodule from the declared "stuff" module
use stuff::hello;
fn get_arguments() -> Vec<String> {
// Get the arguments from CLI and do check, then
// return the arguments, without the program name
let args : Vec<String> = env::args().collect();
if args.len() <= 1 {
eprintln!("Please specify arguments !");
std::process::exit(1);
}
// We get a slice of an existing vector
return args[1..]
// but to return it, we need it to be a vector - so cast it back explicitly
.to_vec();
}
fn main() {
let args : Vec<String> = get_arguments();
for name in args.iter() {
println!("{}", hello::greet(name) );
}
}
Then write a src/stuff.rs
file
// This is the side-module "stuff"
// It has submodule implementations in "stuff/"
// declare their existence here. Note that this is
// needed even for each submodule to see each other
pub mod hello;
Then write the submodule in src/stuff/hello.rs
pub fn greet(name:&str) -> String {
// Create a "mutable" and "owned"/heap String from the string-slice (native, on the stack)
let mut phrase:String = String::from("Hello, ");
// Push the stack content from "name" into the head object "phrase"
phrase.push_str(name);
return phrase;
}
Build it, and run it
rustc src/hey.rs
./hey Alex Jay
Lua
Lua is a popular tool scripting language - it's used in a variety of settings notably Roblox, the GNU Image Mainpulation Program ("GIMP"), and if I may just momentarily shill, in Minetest. I used to write and maintain various mods for Minetest, and it's even fair to say I did a lot of my learning during that time... there's a lot of learning that can happen on spare time when you're also having fun 😄
Any application that does embed Lua likely embeds a specific reference version of it, and may add/remove library features as suits the application. For this test I installed the CLI interpreter for Lua 5.4, but any version of Lua would work though with this example.
Most notably though, I bring attention to the fact that top-level symbols are global by default, and that to namespace a function, you need to declare it on an existing variable.
In ./hey.lua
-- Executes a file , any non-local symbols enter the global space
dofile("stuff/hello.lua")
if #arg == 0 then
print("Please supply arguments")
os.exit(1)
end
for _,name in ipairs(arg) do
phrase = stuff_hello.greet(name)
print(phrase)
end
And in ./stuff/hello.lua
-- This is a global variable that will go into the global space
stuff_hello = {}
-- This implements a method onto the stuff_hello object/table
-- for namespacing
function stuff_hello.greet(name)
return "Hello, "..name.."!"
end
And run
lua hey.lua Alex Jay Pat
Java
We love to rant about how verbose Java is, but having rattled through a few of the other languages now, it should be possible to see how comparing simple "Hello World" examples does it undue disservice.
Yet there are still some funky constraints. All files should be in a top-level package directory, and your current working directory should be its parent. Every file contains a public class, and each public class must be in a file with an exact corresponding name.
In helloyall/Hey.java
write:
package helloyall;
import helloyall.stuff.Hello;
public class Hey {
public static void main(String[] args) {
Hello greeter = new Hello();
// Actual arguments start from zero - program name is
// not stored in the args array
if(args.length < 1) {
System.out.println("Please supply arguments");
return;
}
for(int i = 0; i < args.length; i++) {
String phrase = greeter.hello(args[i]);
System.out.println(phrase);
}
}
}
And in helloyall/stuff/Hello.java
write:
package helloyall.stuff;
public class Hello {
public String hello(String name) {
return "Hello "+name;
}
}
Compile and run:
# So that java knows where to import from, CLASSPATH needs to also include local project directory
export CLASSPATH=".;$CLASSPATH"
# Compile
javac helloyall/Hey.java
# Run - uses package notation
java helloyall.Hey Jay Alex Sam
Groovy for Jenkinsfile
This is one that stumped me for ages - I had a right rant at Jenkinsfile Groovy a few years back for just how hard it was to find decent concise information about it.
Groovy is a language designed to be a "scripting language" that runs on the JVM, which means some of its actual representations are heavily influenced by Java's limitations requirements.
One of the key concepts to retain first is that the top level Jenkinsfile code itself runs on the server. A bad Jenkinsfile can crash a Jenkins server, so don't let just anyone write Jenkinsfile code to run on your server. note then that any SCM checkout should always be done on a node.
Another corollary to this is that each statement is passed on from the main server to the node for execution - as in, the main server steps through the Jenkinsfile, and dispatches steps individually over to the execution node. As a corollary to this corollary (get comfortable with deep-nesting in Groovy!), the for
and while
loops are forbidden, in favour of iterators.
A second key concept is to know that every Groovy script is a class with instance methods. This has some very interesting effects which I have touched on before and if you are about to embark on such a journey, may I humbly suggest you read those notes. They are to be essential to getting your head around some oddities you will surely encounter.
Declare your main jenkinsfile at the root of your repo, write ./hey.Jenkinsfile
node("runner-node") {
// "runner-node" should be an agent or label defined on your Jenkins server
// Try to always confine code to a node that is _not_ your master node.
// The hey.Jenkinsfile should be in a git repo !! Or SCM of your choice
// The files must be checked out to a node workspace first
checkout scm;
// We can load from the workspace, assuming your files are at the root of the repo
hello = load "${env.WORKSPACE}/stuff/hello.groovy"
names = env.NAMES.split(",")
// Iteration - not a loop ! - with a closure
names.each { name ->
phrase = hello.greet(name);
println(phrase)
}
}
From the root of your repo, write stuff/hello.groovy
def greet(name) {
return "Hello, ${name.trim()}!"
}
// The whole file gets encapsulated to a class
// and any "naked code" goes into a static run() method of the class.
// We must `return this;` so that the `load` operation receives the loaded instance.
return this;
Commit this code and push it. On Jenkins, create a Pipeline job, and point it appropriately to your script. Make it configurable, and give it a String parameter, into which to place names. Save and choose to "Build with parameters" - you can enter the names now, like Alex, Jay, Sam
and see the result on the console output.
Perl
My search for how to do this turned out to be unexpectedly infuriating. Some many years ago, I did do a job which involved integrating our Perl solution with customer environments, and I do not remember it being this maddening...
Searching the web, I get a bunch of results as usual for using other peoples' modules, but nothing for organising my own code.
StackOverflow tells me to RTFM, but the FM is so jargonic that it might as well be an implementation spec for the feature itself rather than a user manual. An attempted answer simply doesn't seem to work, bailing with Can't locate A/Calc.pm in @INC
, and same with a simpler answer on StackOverflow . It pointed also to more documentation perlmod. Some blogger AlivnA gives a pointer which helps get past the @INC
issue, but the subroutine still doesn't seem to export, and at this point I've given up.
This is just a sample of the hair-pulling infuriation trying to hunt down how to import a damn file from a subfolder. It seems this is forbidden voodoo. Perlists, please prove me wrong, I beg of you...
EDIT: The gods have answered, or at least, my friend @pozorvlak took pity on my plight and put together a working Hello Y'All Perl example . The incantations give rise.
I ended up with this 🛑 non working code.
In ./hey.pl
I have
# Add to @INC list
use lib './stuff';
# Import from anywhere in the @INC list
use hello;
# Results in "undefined subroutine &hello::greet"
my $phrase = hello::greet("Jay");
printf("$phrase\n");
and in ./stuff/hello.pm
I have
package stuff::hello;
our sub greet {
my $name = shift(@_);
return "Howdy, $name!";
}
1;
And I run with
perl hey.pl
# Undefined subroutine &hello::greet called at hey.pl line 4.
How is it so difficult to do this supposedly extremely commonplace activity when the OCR of splattered paint resolves to runnable perl 93% of the time ???
Bash for Bash Builder
If you'll indulge me, I've kept this one til last. I wrote this tool during a time I had shell scripting as my main language, being more sysadmin-oriented. One thing I wanted was to be able to write various scripts, but with re-usable code, instead of copy-pasting it around.
source
-ing code is unreliable - it depends on the user's current working directory, and sourced files do not have the benefit of knowing where they themselves are - $0
only ever hints to the top level script's location. It was important to be able to perform imports from known absolute locations.
As such, exporting BBPATH=/path/to/bash/libs
is important, and taken care of by the installer into the ~/.bashrc
file.
With that done, write a ./hey.sh
:
# The out.sh lib allows doing colourful output
#%include std/out.sh
#%include ./stuff/hello.sh
if [[ -e "$1" ]]; then
out:error "No argument set"
exit 1
fi
# Did you know that ":" is a legitimate character for a bash function name?
# Any character that can be a file character can be used.
# Used here for namespacing.
phrase="$(hello:greet "$1")"
out:info "$phrase"
And also write a ./stuff/hello.sh
file:
# Bash-Builder provides some syntax sugar macros for putting names on
# positional arguments. The rest remain in the "$@" array variable
$%function hello:greet(name) {
echo "$name"
}
Build it and run it:
bbuild hey.sh bin/hey
bash bin/hey
Note that even this works, due to the imports being resolved at build time:
cd stuff
bash ../bin/hey
Changing directory and calling like that does not work with regular bash sourcing. Sourcing was one of the major issues I had started developing bash-builder
to solve. Once I had the macros for the inclusion work, it was only a small step to include the syntactic sugar macros of %function(name1 name2 ...)
Arguably, a lot of the richness comes from its associated project bash-libs
which provides a sort of standard library for extra goodiness ...
Conclusion
There we are. A whirlwind tour of a dozen languages demonstrating a Hello-World that's a bit more useful than the basic.
If you write language documentation and tutorials, I hope this inspires you to put something like this in the introduction or first chapter, to give a better flavour of the language.
Any popular languages not on this list ? What does your list of languages look like implementing "Hello Y'All"
Posted on August 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 15, 2024