Skip to main content

SML Syntax Cheatsheet

By David Sun, February 2021

Built-in Types

Six of the built-in types commonly encountered:

ExpressionType
0int
"foo bar"string
#" "char
truebool
1.0real
()unit

Structured Types

Make tuples and lists using built-in types and themselves.

ExpressionType
(15,150)int * int
[1,2,3,4]int list
[["foo","bar"],["baz"]]string list list
((true,1),(false,0,()))(bool * int) * (bool * int * unit)
[(0,1),(1,0)](int * int) list
([#"a",#"b"],[3.14])char list * real list

Note: 1-tuples don't exist in Standard ML.

Operators

Operators have different priority levels. Higher priority operations are performed before lower priority operations. The operators *, +, and - work on both int and real types.

OperatorMeaningPriorityExample ExpressionEvaluates ToNotes
*numeric multiplication78 * 324
/real division73.0 / 2.01.5operands must be real
divinteger divison73 div 21operands must be int
mod"modulo"78 mod 32operands must be int
+numeric addition63 + 47
-numeric subtraction63 - ~25~ denotes negative numbers, e.g. ~5
^string combination6"foo" ^ "bar""foobar"
::list construction ("cons")51 :: [2,3,4][1,2,3,4]right-associative
@list combination ("append")5[1,2] @ [3,4][1,2,3,4]right-associative

Except for :: and @, the remaining built-in operators above are left-associative. Left-associative operations of equal priority implicitly evaluate from left to right. Right-associative operations of equal priority implicitly evaluate from right to left.

ExpressionImplicit InterpretationEvaluates To
3 - ~2 + ~5((3 - ~2) + ~5)0
1 :: 2 :: 3 :: []1 :: (2 :: (3 :: []))[1,2,3]
1 :: [] @ 2 :: [] @ 3 :: []1 :: ([] @ (2 :: ([] @ (3 :: []))))[1,2,3]

Boolean Operation

There are three main ones: andalso, orelse and not.

ExpressionEvaluates ToNotes
false andalso truefalseandalso short-circuits if left operand evaluates to false
true orelse falsetrueorelse short-circuits if left operand evaluates to true
not truefalse
not falsetrue

Note: See the page about the bool type here for more information on short-circuiting behavior.

There are built-in equality operators: = and <>.

OperatorMeaningPriorityExample ExpressionEvaluates To
="equal to"41+2 = 4-1true
<>"not equal to"4"a" <> "b"true

These two operate on equality types, which include the built-in types mentioned before — and the structured types that can be made from them — excluding real and function types.

ExpressionEvaluates To
(true,true) = (true,true)true
0 = 1 andalso 1 = 1false
0 <> 0 orelse 1 <> 1false
[1,2,3,4] = [1,2,3,4]true
(1,2,"a","b") = (1,2,"a","b")true
([1,2,3,4],(["a","b"],[()])) = ([1,2,3,4],(["a","b"],[()]))true
0.0 = 0.0N/A: Not Well Typed

Note: See the page about the real type here for more information on why 0.0 = 0.0 is not allowed.

There are built-in comparison operators >, <, >=, and <=.

OperatorMeaningPriorityExample ExpressionEvaluates To
>"greater than"4"ba" > "ab"true
<"less than"4"ab" < "abc"true
>="greater than or equal to"4#"a" >= #"A"true
<="less than or equal to"4"cab" <= "cba"true

These have limited use; they operate on int, string, char, real.

To build good habits, please practice using the built-in comparison functions Int.compare, String.compare, Char.compare, and Real.compare to compare their corresponding types instead of exclusively using these equality and comparison operators.

Comparison Functions

There is an order type with three values: LESS, EQUAL, and GREATER.

ExpressionType
LESSorder
EQUALorder
GREATERorder
Int.compareint * int -> order
String.comparestring * string -> order
Real.comparereal * real -> order

Example use:

ExpressionEvaluates To
Int.compare (~1,0)LESS
Int.compare (0,0)EQUAL
Int.compare (1,0)GREATER
String.compare ("abc","bac")LESS
String.compare ("cba","cb")GREATER
Real.compare (0.0,0.0)EQUAL

Sometimes you want to compare data that is not of basic built-in types, e.g. when sorting lists of tuples. The built-in comparison operators by themselves will not work, but using the order type allows you to write a comparison function (perhaps using other comparison functions) that defines your own order over that data.

Comments

Comments are denoted using (* *).

(* This is a comment. *)

(* This is another comment.
Comments can span multiple lines!
*)

(* This is (* a comment within *) a comment. *)

Value Binding

Use the val keyword to create variables. It binds values to identifiers (variable names).

val x : int = 5
val (a,b) : int * int = (1,2)
val L : int list = [3,4]
ExpressionEvaluates To
x5
a1
b2
3 * x15
2 * x * (5 + 2 * x)150
a * x + b7
a :: b :: L[1,2,3,4]

Let-Expressions

Create local bindings (local variables) to compute a let-expression. Place declarations and bindings between the let-in; the let-expression between the in-end. Can be nested. The scope of the let-in declaration is that let-expression's expression.

Expression Evaluates To
let
val x : int = 25
val x : int = x + 25 (* Shadows the previous x binding *)
val y : int = x + 50
in
x + y
end
150
let
val x : int = 25
in
let
val x : int = x + 25 (* Shadows the previous x binding *)
in
let
val y : int = x + 50
in
x + y
end
end
end
150

Lambda Expressions

Write lambda expressions using the fn keyword — often verbalized as "lambda". A lambda expression is of the form: fn, pattern, =>, expression. The lambda expression itself is a value a value of function type. The => in lambda expressions correspond to the -> in their types. The -> arrows are right-associative infix type constructors denoting function types. Apply lambda expressions via prefix application — before the immediate operand.

ExpressionEvaluates ToType
(fn (x : int,y : int) => x + y)(fn (x : int,y : int) => x + y)int * int -> int
(3,4)(3,4)int * int
(fn (x : int,y : int) => x + y) (3,4)7int

Function Binding

Using a lambda expression more than once requires retyping it. We can give it a name instead. The val and fun keywords bind a lambda expression to an identifier, creating a named function. Take note that the = for binding differs from the => reserved word.

(* add : int * int -> int *)
val add : int * int -> int = fn (x,y) => x + y

(* add : int * int -> int *)
fun add (x : int,y : int) : int = x + y

Both function bindings for add above have the same value: (fn (x,y) => x + y) : int * int -> int.

A named function can be thought of as a lambda expression that has been "identified". The bindings to add identify (or name) an otherwise anonymous function. Its value is the lambda expression it binds.

ExpressionValueType
add(fn (x,y) => x + y)int * int -> int
(fn (x,y) => x + y)(fn (x,y) => x + y)int * int -> int
add (3,4)7int
(fn (x,y) => x + y) (3,4)7int

Patterns and Case Expressions

Patterns are present in every lambda expression, case expression, val, and fun binding. Every fn clause, case clause, and fun clause contains a pattern with a corresponding expression. A clause is of the form: pattern, =>, expression. Clauses are delimited by pipes |.

Expression Example Clause Clause Pattern Clause Expression
(fn (x,y) => x + y)

(x,y) => x + y

(x,y)

x + y

(fn true  => 1
| false => 0)

true => 1

true

1

(fn 0 => true
| _ => false)

_ => false

_

false

Lambda expressions and case expressions have the same clause syntax. The clausal patterns must be able to match to the type of the expression being cased on. The clausal expressions must all have the same type (which may be different from that of the expression cased on).

Expression Example Clause Clause Pattern Clause Expression
(case () of
_ => ())

_ => ()

_

()

(case #"A" < #"a" of
true => ":)"
| false => ":(")

true => ":)"

true

":)"

(case Int.compare (1,0) of
LESS => false
| EQUAL => false
| GREATER => true)

GREATER => true

GREATER

true

The wildcard pattern _ will match to any type, but create no bindings (ignore it).

CandidateValid Pattern?
()Yes
0Yes
":)"Yes
trueYes
EQUALYes
3 + 4No
":" ^ ")"No
3 < 4No
Int.compare (0,0)No
Int.compareNo
0.0No
(fn x => x)No
xYes
any variable name that is not a reserved wordYes
_Yes
(0,1)Yes
(x,y)Yes
(_,_)Yes
(x,x)No
[]Yes
[x]Yes
[[[]]]Yes
([],[])Yes
[] @ []No
[x] @ xsNo
L @ RNo
x::xsYes
x::y::xsYes
_::_Yes

A pattern that accounts for every possible value of the type it matches to is said to perform an exhaustive match. The match is nonexhaustive if and only if a possible value of that pattern's type is missed.

Expression Pattern Type Exhaustive Match?
(fn () => ())

unit

Yes

(fn true => 1)

bool

No

(fn true  => 1
| false => 0)

bool

Yes

(fn LESS => ~1)

order

No

(fn LESS  => ~1
| EQUAL => 0)

order

No

(fn LESS    => ~1
| EQUAL => 0
| GREATER => 1)

order

Yes

(fn 0 => true)

int

No

(fn 0 => true
| _ => false)

int

Yes

(fn x::_ => x + 1)

int list

No

(fn [] => 0
| x::_ => x + 1)

int list

Yes

(fn (0,b) => true andalso b)

int * bool

No

(fn (0,b) => true andalso b
| (n,_) => false)

int * bool

Yes

Using a wildcard for the first clause's entire pattern produces an exhaustive match.

Recursive Function Binding

The rec reserved word enables a lambda expression to self-reference within its body. The fun reserved word allows self-reference by default. The clause patterns and expressions in fun clauses are separated by = instead of =>.

val rec length : int list -> int = fn [] => 0 | _::xs => 1 + length xs

fun length ([] : int list) : int = 0
| length (_::xs : int list) : int = 1 + length xs

As before, both length bindings have the same value. Don't forget about the lambda!

Expression Value Type

length

(fn [] => 0 | _::xs => 1 + length xs)

int list -> int

length []

0

int

length [1,2,3,4]

4

int

Conditional Expressions

Require a condition that evaluates to true or false, after the if. Two expressions of the same type — one for the then-branch, one for the else-branch. Note that if-then-else expressions only evaluate one of its two branches — the one it takes.

ExpressionEvaluates To
if 0 <> 1 then "foo" else "bar""foo"
if 0 = 1 then (if true then 1 else 2) else (if false then 3 else 4)4
if true then 1 else (1 div 0)1
if false then (1 div 0) else 00

op

The op keyword converts a binary infix operator to binary prefix operation. Priorities are kept the same as before.

ExpressionEvaluates To
(op *) (8,3)24
(op +) (3,4)7
(op ^) ("foo","bar")"foobar"
(op ::) (1,[2,3,4])[1,2,3,4]
(op @) ([1,2],[3,4])[1,2,3,4]

as

If convenient, we can use the as keyword between a variable and a structured pattern to reference a structured value both as a whole and by its constituents. The pattern to the left of as must be a variable. It can be nested. It is always part of a pattern.

val tuple as (a,b) : int * int = (1,2)
Variable NameBound To
a1
b2
tuple(1,2)
val outer as (inner as (a,b),c) : (int * int) * int = ((1,2),3)
Variable NameBound To
outer((1,2),3)
inner(1,2)
a1
b2
c3
val L1 as x1::(L2 as x2::(L3 as x3::L4)) : int list = [1,2,3]
Variable NameBound To
L1[1,2,3]
x11
L2[2,3]
x22
L3[3]
x33
L4[]