Anthony Michaelson

Chop wood, carry water

Crossing the Language Barrier

In the vast, intricate cosmos of computer programming, the journey to mastering the art of coding often begins with a profound realization: the syntax of a programming language, while important, is but a mere vessel for the deeper concepts that form the backbone of all programming. This essay is about transcending the surface-level intricacies of syntax, embracing the universal principles of computer science, and understanding how to select the most suitable language for a given task.

Focus on Concepts

Consider syntax as the unique dialects within a singular, universal programming language. Each language, be it Python with its straightforward, English-like clarity or Rust with its focus on efficiency, has its distinctive style of communicating instructions. Yet, beneath this array of syntaxes, there exists a shared, foundational grammar – an ensemble of core concepts that transcends all programming languages.

When you comprehend these underlying principles, you realize that, irrespective of the specific language you’re using, you’re consistently interacting with the same fundamental concepts. These include control structures (like if-else statements and loops), various data structures (such as arrays, lists, and trees), algorithms, etc. These elements are the bedrock of programming, and mastering them equates to accessing a deeper understanding that underpin all software development.

From Syntax to Symphony: Comparing Grammars

As you embark on this journey, remember that learning a new programming language is not starting anew, but rather, it’s adding another instrument to your orchestra. Each language allows you to compose the symphony of your project in a unique way, yet the music’s essence remains rooted in the universal principles of computer science.

For example, consider the logic of processing a common data structure like a binary tree.

# data structure
class Tree:
    node_id: integer
    left: Tree (or null)
    right: Tree (or null)
    name: string

# convert a Tree to JSON
function tree_to_json(tree):
    if tree is null:
        return null
    else:
        json_tree = {
            "node_id": tree.node_id,
            "name": tree.name,
            "left": tree_to_json(tree.left),
            "right": tree_to_json(tree.right)
        }
        return json_tree

and compare this to the logical difference of using tail-recursion:

# data structure
class Tree:
    node_id: integer
    left: Tree (or null)
    right: Tree (or null)
    name: string

# convert a Tree to JSON
def tree_to_json(tree):
    if tree is None:
        return None
    return {
        "node_id": tree.node_id,
        "name": tree.name,
        "left": tree_to_json(tree.left),
        "right": tree_to_json(tree.right)
    }

The important element is understanding the logic required to transform a Tree data structure into a valid JSON string. This needs to be done efficiently, regardless of the size of the data input. A lack of understanding of tail-recursion can lead to code plagued by stack overflow errors and slow performance. Worse, it might cause the processing of one system user to adversely impact others.

Programming is about expressing your intent to a computer system and knowing the methods of implementation well enough to design a good overall system.

Once you practice and understand the fundamental concepts in programming, you become language agnostic. Each language may introduce new concepts. You simply add these to your toolbelt. Then, reflect on the other languages you’ve learned to see how these concepts might be expressed, now armed with this new knowledge.

Now let’s take a look at how we might implement our pseudo code in some different languages.

Scala

import play.api.libs.json._

case class Tree(
    node_id: Long,
    left: Option[Tree],
    right: Option[Tree],
    name: String
)

def treeToJson(tree: Tree): JsObject = {
Json.obj(
    "node_id" -> tree.node_id,
    "name" -> tree.name,
    "left" -> tree.left.map(treeToJson),
    "right" -> tree.right.map(treeToJson)
    )
}

Usage:

val myTree: Tree = Tree(
    node_id = 1,
    left = Some(
    Tree(
        node_id = 2,
        left = Some(Tree(4, None, None, "Node 4")),
        right = Some(Tree(5, None, None, "Node 5")),
        name = "Node 2"
    )
    ),
    right = Some(
    Tree(
        node_id = 3,
        left = Some(Tree(6, None, None, "Node 6")),
        right = Some(Tree(7, None, None, "Node 7")),
        name = "Node 3"
    )
    ),
    name = "Node 1"
)

treeToJson(myTree)

This code in Scala is an expression of our intent we communicated in our psudocode but to make it a bit more Scala-esk we’re going to want to implement this in a more idiomatic way to this language.

Idiomatic Scala

package com.example

import play.api.libs.json._

case class Tree(
                 node_id: Long,
                 left: Option[Tree],
                 right: Option[Tree],
                 name: String
               ) {
  def toJson: JsObject = {
    Json.obj(
      "node_id" -> node_id,
      "name" -> name,
      "left" -> left.map(_.toJson),
      "right" -> right.map(_.toJson)
    )
  }
}

Usage:

println(Json.prettyPrint(tree.toJson))

You can see how we’ve adapted the expression of our intent to align with Scala’s idiomatic style. In Scala, it’s common to include data transformation methods with a case class. Each language has its unique patterns, and learning them is essential. This not only allows us to leverage potential performance improvements inherent to different languages but also fosters better collaboration with programmers within that language’s ecosystem.

Tour De Lang

Let’s take a quick tour of some more languages starting with the common to more esoteric.

We’ll note how the syntax features being used become more complex as the languages offer more features. The point of this exercise is to just observe how the concepts we understand from our psudocode are expressed in different grammars. Try to focus on what is the same across all these languages and the differences will also be easier to enumerate.

Javascript

function treeToJson(tree) {
  return {
    node_id: tree.node_id,
    name: tree.name,
    left: tree.left ? treeToJson(tree.left) : null,
    right: tree.right ? treeToJson(tree.right) : null,
  };
}

Syntax Features Used

  • Function declaration and definition
  • Object literal notation
  • Conditional (ternary) operator
  • Dot notation for object property access
  • Parentheses for function calls
  • Curly braces {} to define code blocks
  • Semicolons ; to terminate statements

Typescript

interface TreeNode {
  node_id: number;
  left?: TreeNode;
  right?: TreeNode;
  name: string;
}

function treeToJson(tree: TreeNode): any {
  return {
    node_id: tree.node_id,
    name: tree.name,
    left: tree.left ? treeToJson(tree.left) : null,
    right: tree.right ? treeToJson(tree.right) : null,
  };
}

Syntax Features Used:

  • Interface declaration
  • Type annotations (: number, : TreeNode, : string, : any)
  • Function declaration and definition syntax
  • Object literal notation
  • Conditional (ternary) operator syntax
  • Optional properties (left?: TreeNode, right?: TreeNode)
  • Function parameter declaration (tree: TreeNode)
  • Arrow function syntax ((tree: TreeNode) => any)
  • Object property access syntax (tree.node_id, tree.name, tree.left, tree.right)
  • Conditional statements (if and ? : for conditional checks)
  • Recursive function call (treeToJson calling itself recursively)
  • Null (null) value
  • TypeScript module system (assuming this code is part of a TypeScript module)

Dart

class Tree {
  final int node_id;
  final Tree? left;
  final Tree? right;
  final String name;

  Tree({
    required this.node_id,
    this.left,
    this.right,
    required this.name,
  });

  Map<String, dynamic> toJson() {
    return {
      'node_id': node_id,
      'name': name,
      'left': left?.toJson(),
      'right': right?.toJson(),
    };
  }
}

Syntax Features Used:

  • Class declaration (class Tree)
  • Field declarations with final keyword (final int node_id, final Tree? left, final Tree? right, final String name)
  • Constructor declaration (Tree({ ... }))
  • Named constructor parameters (required this.node_id, this.left, this.right, required this.name)
  • Map data structure (Map<String, dynamic>)
  • Method declaration (Map<String, dynamic> toJson())
  • Object initialization ({ ... })
  • Conditional property access (left?.toJson(), right?.toJson())
  • Dart type system (e.g., int, Tree?, String)
  • Null safety annotations (? for nullable types)
  • Dart’s named argument syntax (Tree({ ... }))
  • Return statement (return { ... })

C

#include <stdio.h>
#include <stdlib.h>

struct TreeNode {
    long node_id;
    struct TreeNode* left;
    struct TreeNode* right;
    char name[50];
};

struct TreeNode* createTreeNode(long node_id, char name[50]) {
    struct TreeNode* newNode = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    newNode->node_id = node_id;
    newNode->left = NULL;
    newNode->right = NULL;
    strcpy(newNode->name, name);
    return newNode;
}

Syntax Features Used:

  • Preprocessor directives (#include <stdio.h>, #include <stdlib.h>)
  • Structure declaration (struct TreeNode)
  • Pointer declaration (struct TreeNode*)
  • Function declaration and definition (struct TreeNode* createTreeNode(long node_id, char name[50]))
  • Dynamic memory allocation (malloc)
  • Type casting ((struct TreeNode*))
  • Assignment (newNode->node_id = node_id, newNode->left = NULL, newNode->right = NULL)
  • String manipulation (strcpy(newNode->name, name))
  • Return statement (return newNode;)
  • Standard C library functions (malloc, strcpy)
  • Data types (long, char)
  • Function parameters (e.g., long node_id, char name[50])
  • Null pointer assignment (NULL)

Rust

use serde::Serialize;
use serde_json::json;
use serde_json::Value;

#[derive(Serialize)]
struct Tree {
    node_id: i64,
    left: Option<Box<Tree>>,
    right: Option<Box<Tree>>,
    name: String,
}

fn tree_to_json(tree: &Tree) -> Value {
    json!({
        "node_id": tree.node_id,
        "name": &tree.name,
        "left": tree.left.as_ref().map(|left| tree_to_json(left)),
        "right": tree.right.as_ref().map(|right| tree_to_json(right)),
    })
}

Syntax Features Used:

  • Rust module system (use serde::Serialize, use serde_json::json, use serde_json::Value)
  • Struct definition (struct Tree { ... })
  • Deriving traits (#[derive(Serialize)])
  • Primitive data types (i64, String)
  • Option type (Option<Box<Tree>>)
  • Box smart pointer (Box<Tree>)
  • Function declaration and definition (fn tree_to_json(tree: &Tree) -> Value { ... })
  • Function parameters (tree: &Tree)
  • References (&)
  • Method chaining (tree.left.as_ref().map(...))
  • Closures (|left| tree_to_json(left)))
  • JSON serialization (json!({ ... }))
  • Rust’s ownership and borrowing system
  • Struct field access (tree.node_id, tree.name, tree.left, tree.right)

Erlang

-module(tree).
-compile(export_all).

-record(tree, {node_id, left, right, name}).

create_node(NodeId, Left, Right, Name) ->
    #tree{node_id=NodeId, left=Left, right=Right, name=Name}.

tree_to_json(Tree) ->
    {struct, [
        {<<"node_id">>, Tree#tree.node_id},
        {<<"name">>, Tree#tree.name},
        {<<"left">>, tree_to_json(Tree#tree.left)},
        {<<"right">>, tree_to_json(Tree#tree.right)}
    ]}.

Syntax Features Used:

  • Module declaration (-module(tree).)
  • Compilation directive (-compile(export_all).)
  • Record definition (-record(tree, {node_id, left, right, name}).)
  • Function definition (create_node(NodeId, Left, Right, Name) -> ...)
  • Pattern matching (#tree{...})
  • Tuple construction ({struct, [...]})
  • Atoms (e.g., <<"node_id">>)
  • Function parameters (e.g., NodeId, Left, Right, Name)
  • Recursive function (tree_to_json(Tree) -> ...)
  • Function clauses (tree_to_json(Tree) -> ...)
  • Field access (Tree#tree.node_id, Tree#tree.name, Tree#tree.left, Tree#tree.right)

Racket

(define-struct tree (node-id left right name))

(define (create-node node-id left right name)
  (make-tree node-id left right name))

(define (tree-to-json tree)
  (if tree
      (let ((left-json (if (tree-left tree)
                           (tree-to-json (tree-left tree))
                           #f))
            (right-json (if (tree-right tree)
                            (tree-to-json (tree-right tree))
                            #f)))
        (list (cons "node_id" (tree-node-id tree))
              (cons "name" (tree-name tree))
              (cons "left" left-json)
              (cons "right" right-json)))
      #f))

Syntax Features Used:

  • Struct definition ((define-struct tree (node-id left right name)))
  • Function definition ((define (create-node node-id left right name) ...))
  • Function parameters (e.g., node-id, left, right, name)
  • Structure constructor ((make-tree node-id left right name))
  • Conditional statement ((if ... ...))
  • Local variable binding ((let (...) ...))
  • Lists and list construction ((list ...))
  • Association lists ((cons ... ...))
  • Predicates (tree, tree-left, tree-right)
  • Converting between types (e.g., (tree-node-id tree))
  • Null value (#f)

Standard ML

datatype TreeNode = TreeNode of {
  node_id: int,
  left: TreeNode option,
  right: TreeNode option,
  name: string
};

fun treeToJson (tree: TreeNode): TreeNode option =
  case tree of
    TreeNode{node_id, left, right, name} =>
      SOME(TreeNode{
        node_id = node_id,
        left = (case left of
                 SOME leftTree => treeToJson leftTree
               | NONE => NONE),
        right = (case right of
                  SOME rightTree => treeToJson rightTree
                | NONE => NONE),
        name = name
      });

Syntax Features Used:

  • Datatype declaration (datatype TreeNode = TreeNode of { ... };)
  • Function definition (fun treeToJson (tree: TreeNode): TreeNode option = ...)
  • Pattern matching (case tree of ...)
  • Record pattern (TreeNode{node_id, left, right, name} => ...)
  • Constructor usage (SOME(TreeNode{ ... }))
  • Recursive function (treeToJson calling itself recursively)
  • Option type (TreeNode option)
  • Null value (NONE)
  • Function parameters (tree: TreeNode)
  • Type annotations ((tree: TreeNode): TreeNode option)

Haskell

data TreeNode = TreeNode {
    node_id :: Int,
    left :: Maybe TreeNode,
    right :: Maybe TreeNode,
    name :: String
  } deriving Show

treeToJson :: TreeNode -> Maybe TreeNode
treeToJson tree =
  case tree of
    TreeNode { node_id = nodeId, left = leftTree, right = rightTree, name = nodeName } ->
      Just TreeNode
        { node_id = nodeId
        , left = case leftTree of
                   Just leftNode -> treeToJson leftNode
                   Nothing -> Nothing
        , right = case rightTree of
                    Just rightNode -> treeToJson rightNode
                    Nothing -> Nothing
        , name = nodeName
        }

Syntax Features Used:

  • Data type declaration (data TreeNode = TreeNode { ... } deriving Show)
  • Record syntax for data types (node_id :: Int, left :: Maybe TreeNode, right :: Maybe TreeNode, name :: String)
  • Type annotations (:: TreeNode -> Maybe TreeNode)
  • Function definition (treeToJson tree = ...)
  • Pattern matching (case tree of ...)
  • Named field pattern matching (TreeNode { node_id = nodeId, left = leftTree, right = rightTree, name = nodeName } -> ...)
  • Data constructor usage (Just TreeNode { ... })
  • Option type (Maybe TreeNode)
  • Recursive function (treeToJson calling itself recursively)
  • Function parameters (tree: TreeNode)
  • Haskell’s type inference and type system

Learning Multiple Languages

Indeed, the intricacies of language features can significantly diverge based on the chosen programming language for translating our pseudocode. Mastering different programming languages is fundamentally about understanding their unique characteristics and subtleties. Once you’ve grasped the core concepts of programming, adapting these to any specific language becomes a more straightforward endeavor.

To gain a comprehensive understanding of the programming landscape, you might only need to familiarize yourself with a handful of languages. However, for a more rounded skill set, consider delving into the following:

  1. Python: Known for its simplicity and readability, Python is excellent for beginners to learn basic programming concepts like loops, conditionals, and data structures. It’s also widely used in data science, machine learning, and web development.
  2. SQL (Structured Query Language): Essential for learning database management and manipulation. It provides a foundational understanding of how to interact with relational databases, query optimization, and data organization.
  3. TypeScript: Extends JavaScript with static typing, allowing students to understand the importance of type systems in programming languages. It helps in learning about compile-time type checking and the benefits it offers for large-scale application development.
  4. Scala: Merges object-oriented and functional programming paradigms. Scala’s functional programming aspects teach immutable data structures, higher-order functions, and type inference, offering a different approach to problem-solving compared to purely object-oriented languages.
  5. Rust: Known for its memory safety guarantees and performance. Rust provides deep insights into system-level programming, memory management, and concurrency, without the overhead of managing garbage collection as in other languages.
  6. Haskell: As a purely functional programming language, Haskell offers a strong foundation in functional paradigms like laziness (lazy evaluation), monads, and function purity, which are less emphasized in more mainstream, imperative languages.
  7. Erlang: Designed for building scalable and fault-tolerant systems, Erlang is particularly instructive for learning about concurrent and distributed programming, as well as the actor model.
  8. Standard ML (SML): As a functional language, SML is great for learning about type inference, pattern matching, and function currying. It also provides a strong foundation in formal language theory and type systems.
  9. Swift and Kotlin: While both are primarily known for mobile app development (Swift for iOS and Kotlin for Android), they also offer unique insights into modern language features like optionals, extensions, and null safety, which are crucial for robust and safe mobile app development.

By exploring these languages, you’re not just learning different syntaxes, but you’re also equipping yourself with a versatile toolkit to approach a wide array of programming challenges and paradigms.

Selecting the Right Language

Choosing a programming language for a project is not merely a technical decision; it’s an act of aligning with the essence of the task at hand. It involves understanding the soul of your project and asking: which language best resonates with this endeavor?

Aspect Considerations
Nature of the Project Consider the terrain you’re navigating. Is it a web application, a desktop software, or a microcontroller for a robotic arm? Python might be your guide in the world of data science, JavaScript could open the gates of web development, while C might connect you deeply with system programming.
Performance Needs Just as different journeys require different modes of transport, different projects have varied performance needs. A language like Rust might be the thoroughbred horse for a race, swift and powerful for high-performance needs, whereas Ruby might be a gentle river, perfect for scripts where speed is secondary to ease and flow of writing.
Language Features The intrinsic characteristics of a language can greatly influence its suitability for a project. For example, Scala’s strong typing and object-oriented features make it a reliable choice for large-scale enterprise applications, while Python’s dynamic typing and readability are perfect for quick prototyping and scripting. Functional programming languages like Haskell offer advantages in scenarios requiring a high level of abstraction and mathematical precision.
Community and Ecosystem Some languages are like ancient trees, with deep roots and a vast canopy – these have extensive libraries, frameworks, and community support. For instance, JavaScript’s ecosystem is an ever-expanding universe, making it a versatile choice for many applications.
Long-term Maintenance Look ahead on the path – will the language stand the test of time for your project? Languages with active communities and ongoing development are like well-trodden paths, easier to maintain and evolve.
Personal Resonance and Team Skills Finally, listen to the harmony between you, your team, and the language. A language that resonates with the collective skills and preferences of the team can turn a laborious march into a graceful dance.

Core Topics for Learning a New Language

Topic Description
Build Tooling Familiarize yourself with the build tools and package managers specific to the language for efficient project management.
Frameworks Explore the most popular or relevant frameworks that facilitate rapid and structured development in the language.
Language Features & Style Guide Understand unique language features and adopt standard coding styles for readability and maintainability.
Idioms Grasp the common idiomatic expressions in the language to write more efficient, readable, and ‘native’ code.
System Libraries Get to know the standard libraries provided by the language for various tasks like networking, file I/O, etc.
Community and Ecosystem Engage with the language’s community for support, and explore its ecosystem for resources, libraries, and tools.

Diving Deeper

Topic Description
Testing & Debugging Learn the standard practices for testing and debugging in the language to ensure code reliability and quality.
Concurrency & Parallelism Explore how the language handles concurrent and parallel programming, essential for high-performance applications.
Cross-platform Development Learn about the language’s capability for cross-platform development and any platform-specific considerations.
Integration Capabilities Understand how to integrate the language with other systems, languages, or frameworks.

Conclusion

It’s challenging to cover such a vast subject in such a short post, but I hope this has served as a worthy starting point for how to approach the landscape of the many programming languages that exist. The most important point that should be derived is to build a map of the landscape based on the capabilities and features of all these languages and core computer science concepts.

A starting point for a fairly comprehensive list of the concepts that one should study to program in any language could be:

Concept Description
Object-Oriented Programming (OOP) Classes, objects, inheritance, encapsulation, polymorphism.
Functional Programming (FP) First-class functions, higher-order functions, immutability.
Type Systems Static typing, dynamic typing, strong typing, weak typing.
Error and Exception Handling Try-catch blocks, error propagation.
Concurrency and Parallelism Threads, asynchronous programming, actor model (Erlang).
Garbage Collection Automatic memory management prevalent in languages like Python, Java, and Kotlin.
Regular Expressions Patterns used to match character combinations in strings, used across various languages.
Lambda Expressions and Anonymous Functions Widely used across multiple languages.
Generics and Type Parameters Generic programming to create flexible and reusable code.
Module and Package Systems Organizing code into reusable components, seen in almost all modern languages.
Event-Driven Programming Building applications that respond to events, prevalent in JavaScript.
Declarative Programming Specifying what the program should achieve rather than how to achieve it, common in SQL.
Asynchronous I/O and Non-Blocking Code Handling I/O operations without blocking program execution, common in Node.js (JavaScript) and Python.
Dependency Injection A design pattern for achieving Inversion of Control, common in object-oriented programming.
Reflection and Introspection Examining and modifying the behavior of programs at runtime.
Memory Safety Concepts like ownership and borrowing in Rust.
Pattern Matching Common in functional languages like Haskell, Scala, and SML.
Higher Order Functions Functions that can take other functions as arguments or return them as results, essential in FP.
Optionals and Null Safety Handling the absence of values safely, as seen in Swift and Kotlin.
Type Inference Automatically deducing the type of an expression, common in Scala and Haskell.
Immutable Data Structures Prominent in functional languages for safer and more predictable code.
Algebraic Data Types (ADTs) Combining types using ‘and’ (product types) and ‘or’ (sum types).
Currying and Partial Application Transforming functions in functional programming languages.
Lazy Evaluation Delayed evaluation of expressions, as seen in Haskell.
Monads A fundamental concept in Haskell for dealing with side effects.
Metaprogramming and Macros Writing code that writes or manipulates other code, seen in Rust and Scala.
Pattern Guards A feature in pattern matching, commonly used in Haskell and SML.
Actor Model for Concurrency A model of concurrent computation in Erlang.
Structural and Nominal Typing Differences in how types are compared and matched in languages.
Reactive Programming and Streams Programming paradigm for asynchronous data streams, seen in Scala and TypeScript.
Memory Management Without Garbage Collection Manual memory management as in Rust.
Tail Call Optimization Specific form of recursion optimization in functional languages.
Recursion and Tail Recursion A function calling itself with optimizations for tail calls to prevent stack overflow.
Type Classes and Higher Kinded Types Advanced type system features found in Haskell and Scala.
Continuations and Coroutine Abstracting flow control in programming, used in Kotlin and other languages.
Domain-Specific Languages (DSLs) Specialized mini-languages for specific problem domains, like SQL for databases.
Software Transactional Memory (STM) A concurrency control mechanism analogous to database transactions, used in Haskell.
Dynamic and Duck Typing Type checking performed at runtime, found in Python and JavaScript.
Data Parallelism and SIMD (Single Instruction, Multiple Data) Running the same operation on multiple data points simultaneously.
Immutable Objects and Persistent Data Structures Objects that cannot be modified after creation, promoting functional programming paradigms.