Beginner

Operators in Standard ML

Learn arithmetic, comparison, logical, string, and list operators in Standard ML, including type-distinct numeric operators and function composition

Operators are how you combine values into expressions, and Standard ML’s operators reveal a lot about its character as a statically-typed functional language. Because SML uses Hindley-Milner type inference and provides no implicit conversions between numeric types, its operators are stricter than those of most dynamic languages: 1 + 1 and 1.0 + 1.0 are valid, but 1 + 1.0 is a type error.

Most SML operators are simply ordinary functions written in infix position. +, ^, and :: are values you can pass around, inspect in the REPL, and even shadow. This is consistent with SML’s functional foundation — there is little magic, just functions with infix syntax and declared precedence.

In this tutorial you’ll learn SML’s arithmetic operators (including the integer-only div and mod), comparison and logical operators, string and list operators, operator precedence, and the function composition operator o. A few things will surprise programmers coming from C-family languages: unary negation is ~ (not -), inequality is <> (not !=), and the boolean connectives andalso and orelse are keywords rather than symbols.

Arithmetic Operators

SML separates integer arithmetic from real (floating-point) arithmetic. Integers use div and mod for division and remainder; reals use /. The unary minus is the tilde ~, which is also how SML prints negative numbers.

Create a file named arithmetic.sml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(* Integer arithmetic *)
val sum  = 7 + 3      (* addition       *)
val diff = 7 - 3      (* subtraction    *)
val prod = 7 * 3      (* multiplication *)
val quot = 7 div 3    (* integer division: 2 *)
val rem  = 7 mod 3    (* modulo:          1 *)
val neg  = ~7         (* unary minus uses ~, not - *)

val () = print ("7 + 3   = " ^ Int.toString sum  ^ "\n")
val () = print ("7 - 3   = " ^ Int.toString diff ^ "\n")
val () = print ("7 * 3   = " ^ Int.toString prod ^ "\n")
val () = print ("7 div 3 = " ^ Int.toString quot ^ "\n")
val () = print ("7 mod 3 = " ^ Int.toString rem  ^ "\n")
val () = print ("~7      = " ^ Int.toString neg  ^ "\n")

(* Real arithmetic uses / for division; div and mod do NOT apply *)
val rdiv = 10.0 / 4.0
val () = print ("10.0 / 4.0 = " ^ Real.toString rdiv ^ "\n")

Notice that Int.toString neg prints ~7: SML uses the tilde for negative literals on both input and output. Mixing types — for example 7 + 3.0 — would be rejected by the compiler, because + resolves to a single numeric type per use.

Comparison and Logical Operators

Comparison operators return a bool. Equality is = (a single equals sign) and inequality is <>. The boolean connectives are andalso and orelse, which short-circuit, plus the function not.

Create a file named comparison.sml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(* Comparison operators return bool *)
val () = print ("5 = 5  : " ^ Bool.toString (5 = 5)  ^ "\n")
val () = print ("5 <> 3 : " ^ Bool.toString (5 <> 3) ^ "\n")
val () = print ("5 < 3  : " ^ Bool.toString (5 < 3)  ^ "\n")
val () = print ("5 > 3  : " ^ Bool.toString (5 > 3)  ^ "\n")
val () = print ("5 <= 5 : " ^ Bool.toString (5 <= 5) ^ "\n")
val () = print ("5 >= 8 : " ^ Bool.toString (5 >= 8) ^ "\n")

(* Logical operators: andalso and orelse short-circuit; not is a function *)
val () = print ("true andalso false : " ^ Bool.toString (true andalso false) ^ "\n")
val () = print ("true orelse false  : " ^ Bool.toString (true orelse false)  ^ "\n")
val () = print ("not true           : " ^ Bool.toString (not true)           ^ "\n")

The = operator works only on equality types — types whose values can be compared structurally, such as int, string, char, bool, and tuples or lists of them. It does not work on real (floating-point equality is unreliable) or on functions. This restriction is enforced at compile time, which is why = on reals is a type error rather than a runtime surprise.

String and List Operators

SML has no overloaded + for strings. Instead, the dedicated ^ operator concatenates strings, and comparison operators work on strings lexicographically. Lists have their own pair of operators: :: (cons) prepends a single element, and @ (append) joins two lists.

Create a file named strings_lists.sml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(* String concatenation with ^ *)
val full = "Hello" ^ ", " ^ "World"
val () = print (full ^ "\n")

(* Strings compare lexicographically *)
val () = print ("apple < banana : " ^ Bool.toString ("apple" < "banana") ^ "\n")

(* List cons (::) prepends one element to a list *)
val nums = 1 :: 2 :: 3 :: []

(* List append (@) joins two whole lists *)
val more = nums @ [4, 5]

(* Turn the list into a comma-separated string for printing *)
val () = print (String.concatWith ", " (map Int.toString more) ^ "\n")

The :: operator is right-associative, so 1 :: 2 :: 3 :: [] builds the list [1, 2, 3] from the right. The difference between :: and @ matters: :: takes an element and a list (int * int list), while @ takes two lists (int list * int list).

Precedence and Function Composition

Like most languages, SML gives *, div, and mod higher precedence than + and -, so multiplication happens before addition unless you add parentheses. As a functional language, SML also provides the composition operator o (a lowercase letter “o”), which builds a new function by chaining two together.

Create a file named precedence.sml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(* Precedence: * binds tighter than + *)
val a = 2 + 3 * 4      (* 14, not 20 *)
val b = (2 + 3) * 4    (* 20 *)
val () = print ("2 + 3 * 4   = " ^ Int.toString a ^ "\n")
val () = print ("(2 + 3) * 4 = " ^ Int.toString b ^ "\n")

(* Function composition with o : (g o f) x = g (f x) *)
fun double x = x * 2
fun inc x = x + 1
val doubleThenFromInc = double o inc   (* applies inc first, then double *)
val () = print ("double (inc 5) = " ^ Int.toString (doubleThenFromInc 5) ^ "\n")

The expression double o inc reads “double after inc”: the rightmost function is applied first. So (double o inc) 5 evaluates inc 5 to 6, then double 6 to 12. Composition is a hallmark of functional programming — it lets you build pipelines of transformations without naming intermediate values.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull eldesh/smlnj:latest

# Run the arithmetic example
docker run --rm -v $(pwd):/app -w /app eldesh/smlnj:latest sml arithmetic.sml

# Run the comparison example
docker run --rm -v $(pwd):/app -w /app eldesh/smlnj:latest sml comparison.sml

# Run the strings and lists example
docker run --rm -v $(pwd):/app -w /app eldesh/smlnj:latest sml strings_lists.sml

# Run the precedence and composition example
docker run --rm -v $(pwd):/app -w /app eldesh/smlnj:latest sml precedence.sml

Expected Output

Running arithmetic.sml:

7 + 3   = 10
7 - 3   = 4
7 * 3   = 21
7 div 3 = 2
7 mod 3 = 1
~7      = ~7
10.0 / 4.0 = 2.5

Running comparison.sml:

5 = 5  : true
5 <> 3 : true
5 < 3  : false
5 > 3  : true
5 <= 5 : true
5 >= 8 : false
true andalso false : false
true orelse false  : true
not true           : false

Running strings_lists.sml:

Hello, World
apple < banana : true
1, 2, 3, 4, 5

Running precedence.sml:

2 + 3 * 4   = 14
(2 + 3) * 4 = 20
double (inc 5) = 12

Key Concepts

  • Numeric types are distinct: integers use div and mod; reals use /. There is no implicit conversion, so 1 + 1.0 is a compile-time type error — convert explicitly with real, floor, round, or trunc.
  • Unary minus is ~, not -. SML also prints negative numbers with the tilde, so ~7 is both how you write and how you see a negative value.
  • Inequality is <> and equality is = (a single equals sign). There is no == or != operator in SML.
  • = requires equality types: it works on int, string, char, bool, and structures of them, but the compiler rejects = on real or on functions.
  • andalso and orelse are short-circuiting keywords, while not is an ordinary function of type bool -> bool.
  • Strings and lists have dedicated operators: ^ concatenates strings, :: prepends an element to a list, and @ appends two lists.
  • Operators are functions in infix position with defined precedence — *, div, and mod bind tighter than + and -, and the composition operator o chains functions right-to-left.

Running Today

All examples can be run using Docker:

docker pull eldesh/smlnj:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining