Programming Languages
There are languages designed by competent academics (SML, Scheme, Miranda, Haskell, Ocaml, F#, Scala3), by talented professionals (Common Lisp, C, Smalltalk, Erlang, Python, Clojure, Go) and by over-ambitious, narcissistic unqualified amateurs (C++, Java), and you can tell.
PHP and Javascript have been “designed” by ignorant (never studied the subject, the principles, and never looked on what decent people did) idiots.
Yes, I know how Stroustrup and Gosling rationalized their decisions (actually - fundamental theoretical fuckups) in retrospect. Both literally ignored everything but Algol, C and Simula. Just basic uniformity and consistency would have eliminated a lot of fundamental problems and pain.
Exactly these fundamental theoretical fuckups are trying to be fixed by Scala, Clojure, Rust (also an amateur crap) and that Google’s take on Rust. They are just trying to apply the results of more than 60 years of PL research, which basically boils down to what Ocaml is.
There are 7 major levels (or contexts) for any programming language: principles, syntax, semantics, idioms, modules, libraries, and tools.
Principles
To understand principles is to understand how and why mathematics works (is). In particular, that arithmetic consist of number types (Sets of numbers) and (together with) a corresponding set of operations (addition, subtraction, multiplication, etc).
It is also important to realize what generalized abstract “structures” (notions) such a Group or a Monoid are.
The main principle in programming languages (just as in Math) is uniformity. It follows naturally from the universal principles of a function- and data- abstractions, which, in turn, is what the Category Theory and the Set Theory are all about respectively.
The Simple Typed Lambda Calculus (has been invented before any digital computers) is an underlying mathematical formalism.
What we call Modularity are just sets of Abstract Data Types, which are named sets of function signatures (exported “public” interface), along with “hidden” (abstracted out by these function signatures) implementation details, packaged into a semi-independent module.
Ideally, such Abstract Data Types (and related modules) should correspond to the major concepts in the problem domain, and to derived generalized abstractions made from analysis and decomposition of the domain.
Last but not least, exact representations of data items (just like 42
or
"+"
on a sheet of paper) and NOT objects should be passed around (just like “in the wire”), and just like in biology, the
notion of an identity of each molecule is redundant (it is defined by
its location) while the notion of being exactly the same as any other
molecule of its kind is crucial. Our invented Numbers follow the same
principles and this is why math “works”.
Uniformity
- Has the Simple-typed Lambda Calculus at its core (LIPSs and ML descendants)
- Everything is an expression (not in Java, not in C++. Ocaml has definitions)
- No implicit coercions (not in Java, not in C++)
- Every value has a type, even a type-tag attached (LISP and ML descendants)
- Every expression is reducible to a value, so it has a type (or an Exception)
- Every value is an expression (in a normal-form - it is self-evaluating).
- Once created, every value is /immutable.
- Every /binding is /immutable (a new binding, which shadows, can be introduced).
- So every compound data-structure is persistent.
- Referential transparency is maintained or even enforced (Haskell).
- The proper Numerical Tower is implemented (only Smalltalk and some LISPs, but not Ocaml)
- Algebraic Data Types and the proper type theory (ML descendants)
- Uniform (everywhere) pattern-matching on data-constructors (ML descendants)
- Uniform asynchronous message-passing (Erlang)
- Single-argument lambdas, currying and partial application (a real uniformity)
- Syntactic sugar for multiple clauses (with patterns) for a function
(SML, Erlang, Ocaml with the
function
keyword) .
Curried functions have the same signature as logical implications, and this is not a coincidence.
Each argument (value) is captured into a closure (accepted as a “fact”) and a new lambda (closure) is returned for the next argument (“fact”). Any curried function “knows” how many and of which type the arguments must be.
Applying a curried function to a fewer arguments (which returns a lambda for the next argument, not the result) is called partial application.
This is not just “natural”, but also a standard idiom - a partially applied functions can be passed around, carrying already captured arguments with it.
Each clause of a function (defined by pattern-matching) can be considered as a partial function - it accepts a particular subset of a whole type, but is itself a pure function nevertheless.
When a clause of a pattern-matching expression (match ... with ...
) introduces new bindings, it
is semantically the same as a partial function (of a particular subset of the type): same input - same
output. Thus is what we call a uniform pattern-matching, almost like in
Erlang (where everything is pattern-matching).
And, of course, pattern-matching is not just a convenient way of destructuring (without throwing exceptions). This is a more general notion of a value to have a certain “shape” (like a molecule) and of matching against a particular shape.
This is what Rust “trannies” missed completely because they never studied the subject, but they are too proud of being “special” and “creative” lmao.
Syntax
Defined by a set of rules, which together specifying what constitutes a well-formed expression.
Syntax-checking is, obviously, done before type-checking and evaluation.
Type declarations
This is the syntax and the rules of how to define new (user-defined) types and especially Algebraic Data Types.
Pattern-matching
Pattern matching on data-constructors (of Algebraic Data Typesp)
Semantics
Basically, semantics are sets of precise rules of how a compiler would “understand” (or an interpreter interpret) a particular kind of expressions (this or that syntactic form). Your understanding must correspond to the compiler’s.
Dynamic semantics
Evaluation rules - how a particular kind of expressions will be
evaluated at runtime.
For example, an if e1 then e2 else e3
expression has its own evaluation
rules (either e2
or e3
, depending on what whether e1
is true
).
Pattern-matching rules
These can be singled out because they are complex and may or may not introduce local scopes and new bindings.
Static semantics
Typing rules - how a particular kind of expressions will be type-checked
by the compiler.
For example, an if e1 then e2 else e3
expression has its own typing
rules (both clauses e2
and e3
has to be of the same type).
Common idioms
The same computation could be expressed in more than one way. The
classic example is how some imperative loops could be expressed as
declarative recursive functions, like map
or fold
.
Idiomatic usage of the comprehension syntax is another way of reducing complexity.
Modules
Exports
We can choose which particular values to export and which to hide.
Imports
This is how we import values from a module (external or internal).
Functors
This defines operations on whole modules which are available in Standard ML, Ocaml and F#.