ML is not only Machine Learning

Functional programming languages have become increasingly popular in recent years for solving complex, high-performance problems. One of the most foundational functional languages is ML (Meta Language), which has influenced the development of many modern programming languages such as Haskell and OCaml. ML stands out for its robust type system, immutability, and powerful recursion capabilities. Originally designed in the 1970s for theorem proving, ML continues to be a go-to language for formal verification, symbolic computation, and beyond.

In this guide, we’ll explore the essential features of ML, including constants, operators, lists, tuples, functions, and the critical importance of strict rules like the mandatory else clause in conditional expressions. By the end, you’ll understand why ML remains a highly regarded functional language and how its design principles can improve your programming practice.

Introduction to ML

ML (Meta Language) was developed by Robin Milner and his team at the University of Edinburgh in the 1970s. Initially, it was intended to serve as the language for implementing theorem provers, which require a high degree of precision and mathematical rigor. As a result, ML has a strong static type system, type inference, and functional purity, making it one of the most predictable and safe programming languages.

ML’s influence can be seen in languages like F#, Haskell, and OCaml, all of which borrow heavily from its syntax and functional design. One of the core principles of ML is immutability: once a variable is bound to a value, that value cannot be changed, encouraging a clean, functional approach to problem-solving.

Key Elements of ML

1. Constants and Basic Data Types

ML supports a variety of constants, each tied to specific data types:

  • Integers: Defined using standard decimal notation, such as 1234.

  • Real Numbers: Real numbers include a decimal point (e.g., 123.45).

  • Booleans: Logical values, represented by true and false.

  • Strings: Represented by text in double quotes, such as "hello".

  • Characters: Single characters are represented by a # prefix, such as #"A".

These constants can be combined with operators to form expressions, which ML evaluates using strict type rules.

2. Operators and Precedence

ML provides a comprehensive set of arithmetic, logical, and string manipulation operators. For example:

  • Arithmetic operators: +, -, *, div for integers, and / for reals.

  • Boolean operators: andalso (logical AND), orelse (logical OR), and not (negation).

  • String concatenation: The ^ operator, used to concatenate two strings.

Operator precedence in ML follows a clear hierarchy to avoid ambiguity:

  • Highest precedence: Negation (~), arithmetic operations (*, /, div), and comparison operators (<, >, =, <>).

  • Lower precedence: Logical operations like andalso and orelse.

ML also employs short-circuit evaluation for Boolean operators, meaning that if the outcome of an expression is already determined by the first operand, the second operand is not evaluated. This improves efficiency, particularly in complex logical expressions.

3. Variables and Immutability

One of ML’s standout features is its insistence on immutability. Variables are defined using the val keyword, and once a value is assigned, it cannot be altered. This is a core concept in functional programming, as it eliminates side effects, making the code more predictable and easier to debug.

Example of defining an immutable variable:

val x = 5 + 3;

Here, the value 8 is bound to x, and x cannot be reassigned a new value.

4. Lists and Tuples

ML supports both tuples and lists for storing collections of values. Tuples are fixed in size and can contain elements of different types, while lists are dynamic but must consist of elements of the same type.

  • Tuples: Defined using parentheses and commas, tuples are useful for grouping related values.

      val coordinates = (10, 20);
      val point = ("label", coordinates);
      val first = #1 coordinates;  (* Extracts the first element, 10 *)
    
  • Lists: Defined using square brackets, lists allow for flexible data storage, with operations such as prepending (using the :: operator) and concatenation (using the @ operator).

      val numbers = [1, 2, 3];
      val moreNumbers = 0 :: numbers;  (* Results in [0, 1, 2, 3] *)
    

In ML, lists are immutable, meaning you cannot modify elements once they are added. Instead, you build new lists by adding or removing elements from existing lists.

5. Functions: The Heart of ML

In ML, functions are central. Defined using the fun keyword, functions are first-class citizens, meaning they can be passed as arguments, returned from other functions, and stored in data structures. Functions can be either simple or recursive.

Simple Function Example:
fun square(x: int): int = x * x;
Recursive Function Example:

Recursion is a fundamental concept in ML, where loops are typically replaced by recursive functions.

fun fact(n: int): int = 
    if n = 0 then 1
    else n * fact(n - 1);

Notice the importance of the else clause in ML's conditional statements. In ML, the else clause is mandatory, ensuring that every conditional expression handles all possible cases, preventing ambiguity in how the program flows.

6. Type Inference and Strong Typing

ML’s type system is one of its most powerful features. It uses strong static typing, meaning that the type of every expression is known at compile time. This eliminates many common errors seen in dynamically-typed languages. Even though ML requires every expression to have a well-defined type, it uses type inference to automatically deduce types for most variables and functions.

For example:

fun add(x: int, y: int): int = x + y;

In this example, ML infers that x and y are integers and that the function returns an integer. The type can also be explicitly declared, providing greater clarity in complex programs.

7. Pattern Matching

One of ML's distinguishing features is its support for pattern matching, which is commonly used with lists and recursive functions. Pattern matching allows you to decompose data structures in a concise and readable way, often replacing complex if-else statements.

Example of pattern matching on a list:

fun sumList([]) = 0
  | sumList(x::xs) = x + sumList(xs);

In this function, sumList recursively adds all the elements of the list. The pattern [] matches the empty list (base case), while x::xs matches a list with x as the head and xs as the tail.

8. Polymorphism and Higher-Order Functions

ML supports polymorphic functions, allowing them to operate on multiple types without being rewritten. For example, the length function can work with lists of any type:

fun length(xs: 'a list): int = 
    if null xs then 0
    else 1 + length(tl xs);

Here, the type 'a indicates that length works for any type of list, whether it contains integers, strings, or other data types.

In addition, ML allows for higher-order functions, which are functions that take other functions as arguments or return functions as results. This provides immense flexibility and is a hallmark of functional programming.

Example:

fun applyTwice(f: int -> int, x: int): int = f(f(x));
val result = applyTwice(square, 3);  (* result is 81 *)

9. Garbage Collection

ML comes with automatic garbage collection, which means that memory management is handled by the system. When memory is no longer needed, ML's garbage collector reclaims it, ensuring efficient memory usage and freeing the developer from manual memory management tasks.

Rules and Best Practices in ML

ML enforces several strict rules to maintain clarity and prevent ambiguity:

  1. Mandatory Else Clause: The else clause in conditional expressions is required to avoid ambiguity. Every possible case must be accounted for in control flow.

  2. Immutability: Once variables are defined using val, they cannot be reassigned. This immutability simplifies reasoning about program behavior.

  3. Pattern Matching: Preferred over deep nesting of if-else statements for clean, readable, and efficient code.

  4. Polymorphism: Functions can be designed to work on multiple types without being rewritten, adding flexibility to the code.

Conclusion: Why Learn ML?

ML offers a rich set of features for functional programming, from immutable data structures and pattern matching to polymorphism and type inference. Its clear syntax and strict rules prevent many common programming errors, making it a great language for both academic purposes and real-world applications. Whether you're working on complex recursive

algorithms, mathematical logic, or formal verification, learning ML will equip you with the tools and concepts that form the foundation of functional programming.

By mastering ML’s strict rules—like the mandatory else clause and the immutability of variables—you can improve the structure and reliability of your code, ensuring clarity and predictability in every program you write.

Did you find this article valuable?

Support Dristanta"s Blog by becoming a sponsor. Any amount is appreciated!