What I Learned Implementing the Same Program in Seven Languages

Recently, I implemented a small web application in PHP, Rust, Go, Python, Ruby, C#, and Java. In this article, I'm sharing some of what I learned during that process.

Want to learn more about Docker?

Are you tired of hearing how "simple" it is to deploy apps with Docker Compose, because your experience is more one of frustration? Have you read countless blog posts and forum threads that promised to teach you how to deploy apps with Docker Compose, only for one or more essential steps to be missing, outdated, or broken?

Check it out

One of the tasks that I took on at work, recently, required updating the details of an existing code project. The project originally showed how to send SMS in each of the seven languages mentioned, except Rust. Now, however, it needed to show how to reply to an SMS as well.

I've worked with each of the language over the years, except for C#, so thought that it would be a fun challenge to brush up my skills in each one, and to learn just enough about C# to complete the implementation.

Over the course of implementing the required functionality in each language, I had the unexpected pleasure of learning a lot about each one, and seeing many of their similarities and differences.

So, in this post, I'm sharing some of what I uncovered. I hope you'll be as interested in what I learned as I was.

What does the code do?

The purpose of the code is to demonstrate how to send and how to reply to an SMS using Twilio's Programmable Messaging API.

Quoting the documentation, the API lets you:

Send alerts and notifications, promotions, and marketing messages on your customers’ favorite channels [SMS, MMS, RCS, and WhatsAPP] with one API. The Programmable Messaging API includes software for managing phone numbers, deliverability, compliance, replies, and more.

Each implementation is composed of, essentially, two pieces.

  • The first is a small command-line app that shows how to send an SMS to a nominated phone number. If you're a Star Wars fan, you'll appreciate the message.
  • The second is a small web app, built using a Sinatra-inspired framework, that shows how to respond to an incoming SMS. I chose to use Sinatra, or a framework inspired by it, such as Slim, (except for the Go implementation), as it's extremely minimalist. Once the web app is running and exposed to the internet, the user can configure Twilio to send a webhook (a GET or POST request) to one of the app's two routes, in response to an incoming SMS.

How does the web app work?

The first route has the path /receive/no-response. Requests to this endpoint, basically, do nothing. They just accept that an SMS was received. The second route has the path /receive/with/response. Requests to this endpoint will use TwiML (Twilio Markup Language) to instruct Twilio to send a reply SMS to the sender.

If you've not heard of TwiML before, it's:

An XML-based language that instructs Twilio on how to handle various events such as incoming and outgoing calls, SMS messages, and MMS messages

Here's an example of some TwiML that will send an SMS to a number with the message "We got your message, thank you!".

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Message>We got your message, thank you!</Message>
</Response>

Honestly, it's one of the best parts about working with Twilio. But, that's a story for another day.

The message in the reply SMS is randomly generated from a list of predefined option, based on the contents of the incoming SMS. Specifically, if the message body is "never gonna", then it will send back a suitable follow-up line from Never Gonna Give You Up, by Rick Astley. If it's anything else, a default response is sent back.

What did I need from each language and accompanying framework?

Given the simplicity of the application's design, I identified the following requirements that I would need from each language and it's accompany framework or package.

The ability to:

  • Generate a random integer
  • Define a default response string and a list of potential response strings
  • Easily parse a POST request body
  • Define routes and link them to a handler function that would be called when the route was requested
  • Perform simple string manipulation
  • Set a response body, status code, and content type
  • Generate the required TwiML

That's not a particularly intense list of requirements, to be fair. So, I didn't see it being an issue in any of the given languages.

How did I implement the code in each language?

Given that the focus of the repositories is on demonstrating how to send and reply to SMS, as I noted earlier, I felt that the best approach was to pick a minimalist framework, ideally, one that was inspired by Ruby's Sinatra framework.

If you're not familiar with the framework, it let's you create web apps and APIs quickly, often with a minimum of effort. For example, here's all the code you need to create an app that handles GET requests

require 'sinatra'

get '/frank-says' do
    'Put this in your pipe & smoke it!'
end

If you're not familiar with Ruby, first, Sinatra is imported; I'll use the term "imported" for the remainder of this post, as that's the terminology I'm most familiar with. After that, a route is registered that responds to the HTTP GET method with the path /frank/says. When that route's requested, the text "Put this in your pipe & smoke it!" is sent as the body of the response.

To run the app, you then need to run the following command, where <file-name> is replaced by the name of the file that you put the code in.

ruby <file-name>.rb

The app will then launch and bind to port 4567 on your local machine if the port is not in use. With the app running, you could use a networking tool such as curl or resterm to make a request to it and see the text returned as the body of the response.

That's why I had Sinatra-like frameworks as my frame of reference. They let me write just the minimum code required to define routes, parse request bodies, and set relevant response headers, while keeping the core focus be on the Twilio-specific integration code.

Which frameworks did I use?

After a bit of research, such as in Reddit forums and on Stack Overflow, here's the list of languages and frameworks used:

Language Framework/Package
Go The net/http package
.Net/C# ASP.Net Core Minimal API
PHP The Slim Framework
Python Flask
Rust Axum
Ruby Sinatra

Each language is linked to the language-specific implementation on GitHub.

Of all of these, I found Core Minimal API the most challenging, largely as I was learning it from scratch, and because I've had very little C# experience. I think it's a fine framework, from what I've seen. But, I think that it's not the easiest to get started with for newcomers to the Microsoft ecosystem.

Which language was the easiest to use?

Of all the languages, PHP was the simplest. This makes sense as, of all the languages, I have the most experience with PHP; starting way back in 1999. This was helped by the Slim framework just making building web apps and APIs so easy!

Given that, I didn't have to spend much time asking questions, looking through documentation, or figuring out how something works in the language. If I checked for anything, it was just a quick check of the parameters to a function, which I was already pretty sure I knew, anyway. Must memory counts for a lot

Which language was the most challenging to use?

The most challenging language — perhaps surprisingly — was not Rust, but C#. I'd suggest that this is the case because I have rarely used either the language or the ASP.Net Core Minimal API framework.

In the time since I started writing this post, I've been pondering about what made working with the language so challenging. Of all the points that I thought of, only one makes sense. While the respective documentation is very thorough, having barely any experience with the language, the tooling, the framework, or the Microsoft ecosystem required a lot more investment than the other languages and frameworks.

What's more, the nomenclature that the language employs is different enough from the other languages, that it slowed me down while I double-checked that it did mean what I thought it mean. For example, they use the term "dispatcher" for the function that is called when a route is requested, where I'm used to having it referred to as a "handler"

A notable second reason is that I've never really had a positive experience of anything that's come out of Redmond. Dating right back to my early days using Windows 95 A, and then continuing through Windows 98, Me, Windows 7, Microsoft Teams, Microsoft Access, Visual Basic for Applications — and on and on the list goes.

That's why I moved to Linux!

Seriously, decades later, I have so much built up negativity towards anything Microsoft, from numerous experiences, that it's hard to set that aside and approach the language with an open mind.

That said, I worked hard to be open-minded to both the language and the framework, and for the most part, feel that they're solid offerings.

What did I learn?

Answering "what didn't I learn" would be easier - and significantly quicker. But, it would be nowhere near as interesting for you, my dear reader. So, let's work through what I learned porting the application across multiple languages.

The first key observation is that — except for Rust — in so many ways it was not always easy to tell the difference from one language to the next. Sure, the syntax of each language identified it (that, and the file extension and project directory name), so I mean this only loosely.

But, seriously. All seven languages share so many similarities, such as language features, the way they import libraries, their shared library structure, their design patterns, and the way their tooling works, that they start blurring in to one another after a while.

Let's step through some of those points, naturally with some examples, so you can see what I mean.

Each language imports symbols quite similarly

Let's start off with importing libraries. I won't provide examples of each language, as I think that would be redundant.

Here's how libraries (or namespaces) are imported in Java:

import com.twilio.Twilio;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.messaging.Body;
import com.twilio.twiml.messaging.Message;
import com.twilio.type.PhoneNumber;

Here's how it's done in JavaScript:

const { MessagingResponse } = require("twilio").twiml;

Here's how it's done in Rust:

use axum::{Form, Router, routing::post};
use rand::prelude::*;
use serde::Deserialize;

And here's how it's done in Go:

import (
 "fmt"
 "log"
 "math/rand"
 "net/http"
 "strings"

 "github.com/twilio/twilio-go/twiml"
)

It's pretty clear what's going on (though how JavaScript does it is a little more opaque). Each language uses a keyword to denote that it's importing one or more packages, classes, or symbols into the current namespace to use in the current file.

Java and Go use import, Rust uses use, and JavaScript uses require. Then, each language (though not shown in each of the four examples) allows for importing either a single or multiple elements, such as Go using parenthesis, and Rust (and PHP) using curly brackets. Then, they each separate namespace paths, whether with forward slashes, double colons, or a period.

Once you've learned how one or two work, to use the others, you just learn the new syntax. Conceptually, they work the same way.

Each language (except Java) has an excellent package manager

Then, there's how their package managers work.

PHP has Composer. Rust has Cargo. Python has PIP. Ruby has Gem. And on the list goes. Each one, from my experience, is excellent because they're feature rich, have excellent documentation, and a well thought out approach to command names.

Each one lets you, among other things:

  • Add a package to the required packages for the current project
  • Remove one or more existing packages from the project
  • List the available packages
  • List why one or more package is there

For example, here's how to add a package with the four, mentioned package managers:

# Add Twilio's PHP Helper library with Composer
composer require twilio/sdk

# Add the anyhow, dotenv, reqwest, and rustlio traits with Cargo, and enable Reqwest's blocking and json features
cargo add anyhow dotenv reqwest rustlio --features reqwest/blocking,json

# Install Twilio's Ruby Helper library, version 7.10.3 with Gem
gem install twilio-ruby -v 7.10.3

# Install the ast-grep-cli package with PIP
pip install ast-grep-cli

The examples install different packages, and highlight slightly different functionality, but otherwise accomplish pretty similar tasks. Given this tooling, the configuration files they maintain in a given project (i.e., package.json, composer.json, go.mod), and the quality of the accompanying documentation, it's pretty trivial to get up and running with each language. I have to stress, this tooling saves significant time and complexity.

The lack of one for Maven (to the best of my knowledge) is one of the main reasons why I found implementing the Java version much slower than the rest.

Having one would have made maintaining the configuration file (pom.xml) easier. Without it, I had to search for packages on [Maven Central], copy the dependencies XML snippet into pom.xml, and then build the project again to add the dependency to the project.

For what it's worth, this thread on Reddit seems to indicate that there is no equivalent package manager, and that copying and pasting XML snippets from Maven Central is all you need to do. 😔

Each language has a rich assortment of data types

Each language has a rich array of core data types, such as arrays, lists, tuples, hash maps, vectors, and strings, as well as their simpler, scalar data types, such as ints, floats, chars, and booleans, etc.

Sure, these should be standard fare for any modern software development language, but it's great that they all, largely, share most of this functionality in common.

Because of that, while the specific implementation from one language to the next may vary slightly (or a little more in the case of Rust, at times), it makes it a lot easier to pick up one language after another.

Here are three short examples from the repositories, showing the how to define a string and a list of strings.

Firstly, in Rust.

let default_option: &str = "I just wanna tell you how I'm feeling - Gotta make you understand";
let options: Vec<&str> = vec![
    "give you up",
    "let you down",
    "make you cry",
    "run around and desert you",
    "say goodbye",
    "tell a lie, and hurt you",
];

The example above first defines a string slice (str) to hold the default message to reply to incoming SMS with. Then, it defines a vector of string slices to hold possible messages to reply to the incoming SMS with, that will be chosen from at random.

Here's the Go implementation:

const defaultResponse = "I just wanna tell you how I'm feeling - Gotta make you understand"
options := [6]string{
    "give you up",
    "let you down",
    "run around and desert you",
    "make you cry",
    "say goodbye",
    "tell a lie, and hurt you",
}

Here, I've defined a string constant for the default response, followed by an array of strings for the random responses.

And, finally, here's how I implemented it in PHP:

$default = "I just wanna tell you how I'm feeling - Gotta make you understand";
$options = [
    "give you up",
    "let you down",
    "run around and desert you",
    "make you cry",
    "say goodbye",
    "tell a lie, and hurt you"
];

Each language has rich function support

As they support a mixture of named parameters/keyword arguments, parameter types, and return types. While they're not shared across all languages (such as Rust, Go, and Java not supporting named parameters), the support for parameter and return types is pretty solid.

Again, let's look at three examples from the respective repositories, just showing the function definitions, not the bodies.

Firstly in Rust:

async fn with_response(Form(request): Form<TwilioMessagingRequest>) -> String {}

Then in Python:

def with_response():

Then in Go:

func handleSendResponse(w http.ResponseWriter, r *http.Request) {}

And then in PHP:

$app->post('/receive/with-response', function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    array $args
) {}

I'm not, deliberately, avoiding showing examples in Python, Ruby, or .NET/C#. The main reason for favouring the other languages is because of how I implemented the examples in those languages. They lend themselves better to providing code examples for this aspect of functionality.

Each language supports Object-Oriented Programming (OOP)

While Rust and Go don't explicitly provide OOP language constructs, PHP, Python, Ruby, C#, and Java do. For example, you can define classes along with their properties and functions/methods, you can define abstract classes, and you can have one class inherit from another.

Here's a simple example in PHP, Python, and Ruby, which models a basic application user.

<?php

class BaseUser
{
    private string $firstName;
    private string $lastName;

    public function getFullName(): string
    {
        return sprintf("%s %s", $this->firstName, $this->lastName);
    }
}

Here's the same example in Python:

class BaseUser:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def getFullName(self) -> str:
        return f"{str(self.first_name)} {str(self.last_name)}"

Here's the same example in Ruby:

class BaseUser
  attr_accessor :first_name
  attr_accessor :last_name

  def initialize(first_name = 'Matthew', last_name = 'Setter')
    @first_name = first_name
    @last_name = last_name
  end

  def get_full_name
    puts "#{@first_name} #{@last_name}"
  end
end

Now, just because that functionality isn't directly available in Rust and Go, it doesn't mean that you can't achieve the same outcome in those languages. In both of these languages, for example:

Regardless, while the former five and latter two differ in how they let you achieve OOP, and while the former five use different terminology in some cases, it's pretty trivial to learn how to achieve the same end in each one.

For what it's worth, OOP isn't the be all and end all, but it's something that I regularly look for.

Each language has rich string support

Perhaps this was a benefit borne out of the application that I implemented. Okay, it likely was. Regardless, each of the languages has rich string support, something that I'm grateful for, as I use strings regularly.

By "rich string support", specifically, what I mean is that they have native string types, have copious functions for working with and manipulating strings, and have numerous ways of building strings.

For example:

Each language has a rich standard library

Each of the languages have excellent standard libraries, ones that are well documented, and easy to get up and running with. To me, this is a hallmark of a language that has grown and developed for long enough, with a community that truly cares about it. Sure, this is a personal opinion, but I feel that it's a fair point to make.

But, more specifically standard libraries:

  • Save you time writing code for fairly common operations. Given it's officially endorsed, you don't have to write nor maintain it yourself, but you get the benefit of using it and building around it.
  • Provide a standardised way to work with the language. Again, as it's officially endorsed, anyone else working with the language should be expected to know and be familiar with it. So, it provides, perhaps, an unofficial way of teaching people how to use the language.
  • Teach the idiomatic ways of working with the language
  • Show how to document your code. Actually, I'm not sure if each of the seven languages does this, but Rust and Go sure do. They're excellent examples of this point.

In addition to these, there are numerous other benefits, but these are a solid starting point.

All of the languages borrow so much from one another

This is likely the point that most impressed me, and made the process of implementing the application across the seven languages so simple. This point shows me that each language is being developed by communities that are open to new and different ideas. Each community is willing to, regularly, look outside of their own proverbial garden and borrow from other communities when they recognise ideas as being valuable.

It doesn't matter whether it's generics, traits, structs, lambdas (or arrow functions), closures, aliases, or copious other concepts, or frameworks that take inspiration from others. Each language is borrowing from or being inspired by the others, resulting in them evolving faster and in a richer way than they otherwise might.

Here are two examples of this point. In this first example, you can see how I implemented the route to send an SMS reply to the incoming SMS in PHP with the Slim framework.

$app->post('/receive/with-response', function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    array $args
) {
    $body = $request->getParsedBody()['Body'];
    $default = "I just wanna tell you how I'm feeling - Gotta make you understand";
    $options = [
        "give you up",
        "let you down",
        "run around and desert you",
        "make you cry",
        "say goodbye",
        "tell a lie, and hurt you"
    ];

    $twimlResponse = new MessagingResponse();

    if (strtolower($body) == 'never gonna') {
        $twimlResponse->message($options[array_rand($options)]);
    } else {
        $twimlResponse->message($default);
    }

    $response->withHeader('Content-Type', 'application/xml');
    $response->getBody()->write($twimlResponse->asXML());

    return $response;
});

The route has the path "/receive/with-response". When requested, it's handled by an anonymous function which takes three arguments:

  • A ServerRequestInterface object that models the current request
  • A ResponseInterface object that models the response to be sent back from the request
  • An array of request arguments (which can, among other things, contain the value of route placeholders, etc)

The function retrieves the Body parameter from the request which contains the body of the incoming SMS, and defines a default response and an array of responses to be picked from at random. Then, it initialises a new MessagingResponse object, which helps create TwiML for sending a message. Then, the message (SMS) body is set, based on the body of the incoming message. Following that, it sets the content-type of the response to "application/xml", sets the created TwiML as the body of the response, then returns the response to the client.

Here's how I implemented in .NET/C#.

app.MapPost(
    "/receive/with-response",
    ([FromForm(Name = "Body")] string body = "") =>
    {
        string defaultOption =
            "I just wanna tell you how I'm feeling - Gotta make you understand";
        string[] options =
        {
            "give you up",
            "let you down",
            "make you cry",
            "run around and desert you",
            "say goodbye",
            "tell a lie, and hurt you",
        };
        int index = new Random().Next(0, options.Length - 1);
        var response = new MessagingResponse();
        var message = new Message();

        message.Body(body.ToLower() == "never gonna" ? options[index] : defaultOption);
        response.Append(message);

        return Results.Text(response.ToString(), contentType: "application/xml");
    }
)
.DisableAntiforgery();

Effectively, the implementation is exactly the same, with two, small changes.

  1. The anonymous function, in this case, uses the FromForm parameter binding. This simplifies retrieving the "Body" parameter from the incoming request and initialising a variable with that value.
  2. It calls the DisableAntiforgery() function to disable anti-forgery protection. I needed to do this in development (afaik) as Minimal API provides request forgery protections by default.

And now, the Ruby implementation.

post '/receive/with-response' do
  body = params['Body'].downcase

  default_option = "I just wanna tell you how I'm feeling - Gotta make you understand"
  options = [
    "give you up",
    "let you down",
    "make you cry",
    "run around and desert you",
    "say goodbye",
    "tell a lie, and hurt you",
  ]

  twiml = Twilio::TwiML::MessagingResponse.new
  twiml.message do |message|
    if body == "never gonna"
      index = rand(1..options.length - 1)
      message.body(options[index])
    else
      message.body(default_option)
    end
  end

  content_type 'text/xml'

  twiml.to_s
end

This is, again, virtually identical to the other two implementations. The only thing that is different, really, is the syntax.

That's a lot of what I learned implementing the same program in seven languages

Honestly, there is so much more that I could share about what I learned on this journey of discovery. But, I feel that, at this point, I'm potentially starting to labour the point and write just for the sake of writing.

However, if you'd like to hear more, I'm more than happy to do so, if there's enough interest. Let me know and I'll see what I can do.

Otherwise, if you're a polyglot developer, what do you struggle with to learn new languages and frameworks, and what do you find helps you out?