Chop wood, carry water
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.
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.
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,
};
}
{}
to define code blocks;
to terminate statementsTypescript
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,
};
}
: number
, : TreeNode
, : string
, : any
)left?: TreeNode
, right?: TreeNode
)tree: TreeNode
)(tree: TreeNode) => any
)tree.node_id
, tree.name
, tree.left
, tree.right
)if
and ? :
for conditional checks)treeToJson
calling itself recursively)null
) valueDart
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(),
};
}
}
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>
)Map<String, dynamic> toJson()
){ ... }
)left?.toJson()
, right?.toJson()
)int
, Tree?
, String
)?
for nullable types)Tree({ ... })
)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;
}
#include <stdio.h>
, #include <stdlib.h>
)struct TreeNode
)struct TreeNode*
)struct TreeNode* createTreeNode(long node_id, char name[50])
)malloc
)(struct TreeNode*)
)newNode->node_id = node_id
, newNode->left = NULL
, newNode->right = NULL
)strcpy(newNode->name, name)
)return newNode;
)malloc
, strcpy
)long
, char
)long node_id
, char name[50]
)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)),
})
}
use serde::Serialize
, use serde_json::json
, use serde_json::Value
)struct Tree { ... }
)#[derive(Serialize)]
)i64
, String
)Option<Box<Tree>>
)Box<Tree>
)fn tree_to_json(tree: &Tree) -> Value { ... }
)tree: &Tree
)&
)tree.left.as_ref().map(...)
)|left| tree_to_json(left))
)json!({ ... })
)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)}
]}.
-module(tree).
)-compile(export_all).
)-record(tree, {node_id, left, right, name}).
)create_node(NodeId, Left, Right, Name) -> ...
)#tree{...}
){struct, [...]}
)<<"node_id">>
)NodeId
, Left
, Right
, Name
)tree_to_json(Tree) -> ...
)tree_to_json(Tree) -> ...
)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))
(define-struct tree (node-id left right name))
)(define (create-node node-id left right name) ...)
)node-id
, left
, right
, name
)(make-tree node-id left right name)
)(if ... ...)
)(let (...) ...)
)(list ...)
)(cons ... ...)
)tree
, tree-left
, tree-right
)(tree-node-id tree)
)#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
});
datatype TreeNode = TreeNode of { ... };
)fun treeToJson (tree: TreeNode): TreeNode option = ...
)case tree of ...
)TreeNode{node_id, left, right, name} => ...
)SOME(TreeNode{ ... })
)treeToJson
calling itself recursively)TreeNode option
)NONE
)tree: TreeNode
)(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
}
data TreeNode = TreeNode { ... } deriving Show
)node_id :: Int
, left :: Maybe TreeNode
, right :: Maybe TreeNode
, name :: String
):: TreeNode -> Maybe TreeNode
)treeToJson tree = ...
)case tree of ...
)TreeNode { node_id = nodeId, left = leftTree, right = rightTree, name = nodeName } -> ...
)Just TreeNode { ... }
)Maybe TreeNode
)treeToJson
calling itself recursively)tree: TreeNode
)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:
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.
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. |
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. |
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. |
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. |