8 May 2023

Programming Languages that changed how I think

This is, in some sense, a part 2 to Software Engineering Books and Resources that I have found interesting. In fact, this list was originally going to be included with that one, but it got too long (especially with the notes), and so I split it out. Some of the notes may get trimmed/edited/fleshed-out at a later date, and I might add more languages.

I have intentionally excluded from this list some of my favourite languages, some of the most popular languages, and some of the best languages for building great products, simply on the grounds that I don't feel that they sufficiently changed the way that I think.

Languages that changed how I think

This isn't a list of recommended languages to use, but rather an incomplete list of languages/language groups that changed how I think about programming.

C

If you haven't already, implement a hash map, a binary tree, a vector, and a linked list in C. (And then never use your own implementations, unless you seriously know what you are doing. (you probably don't, and I certainly don't))

Aswell as having a go with C, I strongly recommend playing with writing some functions in C and calling them from your language of choice, using the FFI. This can sometimes be fiddly (but sometimes straightforward), but being comfortable doing this will add another string to your bow, and improve your understanding of the systems you are using. Once you are confident with this, perhaps try calling a simple statically linked C library (check out the stb libraries and this list). If you want to take things further, you could look at linking with more complex libraries, and linking to shared object libraries written in other languages.

C++ is also a very influential language, and both the exercises above could be done in C++. But I recommend completing them in C first, and then C++, and comparing them.

Sample [Show]
#include <stdio.h>
int main(int argc, char **argv) {
    printf("Arg Count: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("Arg %d: %s\n", i, argv[i]);
    }
    return 0;
}

Related posts

Haskell

Lazy evaluation by default changes how you code - chaining operations / composing functions is often more performant than in other languages

The Hindley–Milner type system makes polymorphism feel natural. OCaml is another great language with a Hindley–Milner type system, but (by default) strictly evaluated. Haskell's Type Classes are a powerful extension to the Hindley-Milner type system.

I find Haskell to be one of the most enjoyable languages to work in. It is also one of the languages that has most changed the way I think about problems.

Sample [Show]
module Main where

splitInHalf :: [a] -> ([a], [a])
splitInHalf = splitAt =<< (`div` 2) . length

main :: IO ()
main = print $ splitInHalf [9,3,3,7,1,2,3]

Related posts

Scheme / Racket

The S-Expression syntax makes the code-data unity more obvious.

Racket is a great starting point for the world of Schemes and Lisps.

Chicken Scheme is a practical Scheme with excellent C and C++ interop.

I previously mentioned SICP and HTDP, if you are interested in Scheme/Racket you should check these out. (SICP) (HTDP).

Clojure is another interesting Lisp language. I particularly like that the although the core language is larger than Scheme, the syntactic sugar is intuitive, and the standard data structures are well chosen and have excellent syntax. Leiningen (a Clojure build tool / project manager) is much more pleasant to use than the other JVM language build tools I have used (SBT/Maven/Gradle).

Sample [Show]

(Scheme / Racket)

(define (mean-average xs)
  (/
    (apply + xs)
    (length xs)))

(display (mean-average '(1 2 3)))

Related posts

Forth

Stack based language, the main benefit is that the execution model is quite simple, so it should be easier to fully understand what is going on.

Forth-style stack based languages are relatively simple to implement interpreters for.

Sample [Show]
2 3 + 5 + 9 dup * * .

Yields 810. Equivalent Infix Notation (e.g. JavaScript):

((2 + 3) + 5) * 9 * 9

Curious.

Assembly and Machine Code

The Little Man Computer (Virtual Machine) or something similar is a good place to start.

Building a computer inside CEDAR Logic Simulator, and creating a Machine Code Language / Opcodes for it (plus an assembly language and assembler!), really helped me improve my understanding of how computers work. This was a great exercise, but unfortunately CEDAR LS only runs on Windows, hasn't been updated since 2020, and the project site seems to have disappeared. If you know of any similar (free) program that runs on Linux, please let me know.

Sample [Show]

x86-64 Assembly:

.helloworld:
	.string	"Hello World"
	.text
	.globl	main
	.type	main, @function
main:
	pushq	%rbp
	movq	%rsp, %rbp
	leaq	.helloworld(%rip), %rax
	movq	%rax, %rdi
	call	puts@PLT
	movl	$0, %eax
	popq	%rbp
	ret	

MiniZinc

A declarative constraint programming language. Another interesting (and much more influential) logic language to look at is Prolog. MiniZinc is listed here instead of Prolog because it influenced me more than Prolog - I have not used Prolog much at all, but I have used MiniZinc professionally.

Read about how I used MiniZinc at Mycs

Sample [Show]
% Input Enums
enum Orders;
enum Suppliers;
enum ProductLines;

% Input Array: ProductLine Sets for each Order
array[Orders] of set of ProductLines: orderProductLines;

% Input Array: ProductLine Sets for each Supplier
array[Suppliers] of set of ProductLines: supplierProductLines;

% Input Array: Prices for Order-Supplier Combinations
array[Orders, Suppliers] of float: orderSupplierPrices;

% Variable Array (To hold results)
array[Orders] of var Suppliers: orderSupplier;

% Suppliers of Orders must have all ProductLines that the Order does.
constraint forall (d in Orders, s in Suppliers) (
    (s != orderSupplier[d]) \/
    (supplierProductLines[s] superset orderProductLines[d])
);

% Variable to optimise (Total Cost)
var float: totalCost = sum(d in Orders) (
    let {var Suppliers: s = orderSupplier[d]}
    in orderSupplierPrices[d,s]
);

% Lower cost preferred
solve minimize totalCost;

% Output as CSV of Order ID and orderSupplier ID pairs
output [ show(d) ++ "," ++ show(orderSupplier[d]) ++ "\n" | d in Orders ];

Erlang

The actor model is worth understanding even if you have no intention of using Erlang, a lot of architecture setups that are quite popular right now are similar to this (e.g. microservices and "serverless" functions). The message passing approach used in Erlang is in some ways closer to Alan Kay's original vision of OOP than languages like Java are.

Sample [Show]
-module(echo).
-export([go/0, loop/0]).

go() ->
        Pid2 = spawn(echo, loop, []),
        Pid2 ! {self(), hello},
        receive
          {Pid2, Msg} -> 
                  io:format("P1 ~w~n", [Msg])
        end,
        Pid2 ! stop.

loop() ->
        receive
                {From, Msg} ->
                        From ! {self(), Msg},
                        loop();
                stop ->
                        true
        end.

Bash

Well, actually, it was Unix Pipes that changed how I think, but this is a list of languages, and I discovered Unix Pipes through Bash.

Sample [Show]

Returns time at which the last change was made to a file in /tmp.

find '/tmp' -printf '%T@\n' 2>/dev/null | sort -r | sed '1q'

Breakdown: find '/tmp' finds files under /tmp, -printf '%T\n' prints the last-modified time of these files, separated by new lines, 2>/dev/null discards any error messages; | passes the resulting list to the next command, sort -r, which sorts the times in reverse (descending) order; | passes this list on again to sed '1q', which prints the first line (i.e. the largest time), and discards the rest.

m4

An interesting macro language. I have found this useful for automatically generating SQL queries. I probably wouldn't use this for a collaborative project, it is a little esoteric these days. But m4 is available on most Unix-style systems, so any program using it should be reasonably portable.

Sample [Show]

An example of using m4 to generate very long and repetitive SQL scripts. In this case, this was to work-around shortcomings in a DBMS' transaction handling/locking system.

changequote(<!,!>)
define(f,
<!\qecho 'Updating Batch $1'
UPDATE netsuite.CUSTOMERS c
SET 
    NAME = tmp.NAME
FROM customerstemp tmp
WHERE c.CUSTOMER_ID = tmp.CUSTOMER_ID
AND tmp.BATCH = $1;!>)dnl

define(forBatches,<!f($1)ifelse(eval($1 < $2),1,<!forBatches(incr($1),$2)!>)!>)dnl


-- Upload Data

CREATE TEMP TABLE customerstemp (
  BATCH               INT8 NOT NULL,
  CUSTOMER_ID   INT8 NOT NULL PRIMARY KEY,
  NAME          VARCHAR(83) DEFAULT NULL
);

\copy customerstemp FROM '/tmp/customer_names_batched.csv' WITH DELIMITER ',' NULL AS 'null' CSV HEADER;

SET statement_timeout TO 360000;

-- Update Table
forBatches(0,100)

Spreadsheet Formulas

e.g. Microsoft Excel, Gnumeric, Google Sheets, LibreOffice. These are powerful and accessible programming languages. You most likely are already familiar with them, but might not think of them as programming languages. These languages have similarities with the Lisp family, especially after the addition of the LAMBDA function to some of them (GSheets and MS Excel).

Sample [Show]
Input:
A
17
29
33
4=SUM(A1:A3)
Result:
A
17
29
33
419

Java

Java changed the way I program for the worse before it changed it for the better. I spent at least a couple of years cargo-culting and swallowing Design Patterns whole before I started to develop a sense of good taste with regards to OOP.

(Java-style) Object-Oriented Programming is particularly useful when you have to maintain many stateful connections to external resources (e.g. printers, different loggers, network services). Interfaces that need to have state. This is because the Java objects paradigm matches this neatly - the external resource type can be represented by a class, and individual connections by objects. In these cases, classes and objects can work quite well.

The problem with Java is that the language encourages the use of this pattern within the software system, and not just at boundaries. This encourages unnecessary complexity, and seems to generally encourage a sort of "object-zoo", within which huge numbers of objects are created, simply because "it seems like what we should be doing".

The "Object" concept is not inherently flawed, but it isn't always the right abstraction. Object-Obsessed Design leads to what John Ousterhout calls "classitis" in "A Philosophy of Software Design". The key symptom of classitis is that the system contains an enormous number of small classes, such that the complexity of their interfaces creates overwhelming whole-system complexity.

There are cases when objects can be the correct abstraction within the system itself. For example, in languages with manual memory-management, such as C and C++, it can make sense to wrap up the allocation and freeing of memory within an object, just as external resources would be. Here, the lack of memory-management in the language/runtime itself creates the need to be careful about accessing the resource. As such this is another example of Object-Orientation being useful when there is a resource that provides functionality, but whose usage must be managed, and each instance of an interface to that type of resource needs to have state in order to do so.

Nowadays, my tendency is towards building a core of pure-functions (where possible) and procedural code, and using objects to wrap external resources. Java can be quite alright, provided you go easy on the classes, inheritance, and reflective dependency injection. Personally I would rather write OOP code in Go, Scheme, or even Haskell, than in Java. I don't think Java really makes it that much easier than in the other three, it just makes it a pain to do anything else.

Sample [Show]
public class Main {
	public static void main(String[] args) {
		System.out.println("Hello World");
	}
}

See also FizzBuzz Enterprise Edition.

Go

Go wasn't in the original version of this list, as I reckoned that it didn't really introduce me to anything mindset-altering that C and Erlang hadn't. But, actually, writing Go, and reading about the rationale for Go, did help convince me of the value of minimalism and radical simplicity in software. This in turn pushed me to adopt a policy/mindset of trying to make a complexity-averse approach my first port of call when solving problems. Which, if I recall correctly, was the core message of one of my very first lectures at university: "KISS" (Keep It Simple, Stupid). That took a while to sink in - I spent most of the next few years trying to do things in the cleverest way possible. Even when things have to be complex, it is still better to let that complexity grow, rather than explode. Go contributed to my eventual realisation that keeping stuff simple was not too bad an idea, so, for that, it gets a place on this list.

Sample [Show]

Go is great for writing web services! Here is a very minimal example:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/hello",
		func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("Hello World!"))
		},
	)
	port := ":8080"
	log.Printf("Starting server, port%s\n", port)
	if err := http.ListenAndServe(port, nil); err != nil {
		log.Fatal(err)
	}
}
Tags: Tech