Beginner

Hello World in Standard ML

Your first Standard ML program - the classic Hello World example with Docker setup using SML/NJ

Every programming journey starts with Hello World. Let’s write our first Standard ML program and explore what makes this influential functional language special.

The Code

Create a file named hello.sml:

1
print "Hello, World!\n";

That’s it! Just one line for a complete Standard ML program.

Understanding the Code

Let’s break down this simple program:

The print Function

1
print "Hello, World!\n";
  • print - A built-in function of type string -> unit that outputs a string to standard output
  • "Hello, World!\n" - The string to print (with \n for newline)
  • ; - Terminates the expression (required in source files at the top level)

Function Application in SML

Like other ML-family languages, SML uses space for function application:

1
2
3
4
5
(* SML style - function applied with space *)
print "Hello, World!\n";

(* Parentheses are for grouping, not function calls *)
print ("Hello, " ^ "World!\n");

The Entry Point

Standard ML doesn’t have a main function. Instead, expressions at the top level of a file are evaluated in order when the file is loaded. The program runs by evaluating these top-level bindings.

For clearer intent, you can write:

1
val () = print "Hello, World!\n";

The val () = ... pattern explicitly shows that we’re executing a side effect that returns unit (similar to void in other languages).

Running with Docker

The easiest way to run Standard ML without installing anything locally is with Docker:

1
2
3
4
5
# Pull the SML/NJ image
docker pull eldesh/smlnj:latest

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

Understanding the Docker Command

  • docker run --rm - Run a container and remove it when done
  • -v $(pwd):/app - Mount the current directory to /app in the container
  • -w /app - Set the working directory to /app
  • eldesh/smlnj:latest - Use the SML/NJ Docker image
  • sml hello.sml - Run the SML interpreter on our file

What You’ll See

When you run the program, SML/NJ will display:

Standard ML of New Jersey (64-bit) v110.99.3 [built: ...]
[opening hello.sml]
Hello, World!
val it = () : unit

The interpreter shows:

  • Version information
  • The file being opened
  • Your program’s output
  • The result type (unit)

Expected Output

Hello, World!

Alternative Approaches

Standard ML provides several ways to output text:

Using print with Concatenation

1
2
val name = "World"
val () = print ("Hello, " ^ name ^ "!\n")

The ^ operator concatenates strings.

Using TextIO for More Control

1
2
val () = TextIO.output (TextIO.stdOut, "Hello, World!\n")
val () = TextIO.flushOut TextIO.stdOut

TextIO provides more fine-grained control over I/O streams.

Formatted Output with Concat

1
2
val greeting = String.concat ["Hello, ", "World", "!", "\n"]
val () = print greeting

Multiple Statements

1
2
3
4
5
val () = (
  print "Hello, ";
  print "World!";
  print "\n"
)

Parentheses group sequential expressions, separated by semicolons.

The Unit Type

In Standard ML, every expression has a type. The print function has type:

1
val print : string -> unit

This means:

  • It takes a string as input
  • It returns unit (written ())

The unit type is SML’s equivalent of void - it represents “no meaningful value.” It’s the return type of functions that exist only for their side effects.

1
2
(* This pattern matches the unit value returned by print *)
val () = print "Hello, World!\n"

Type Inference in Action

Standard ML automatically infers types:

1
2
3
4
5
(* SML knows this is string -> string *)
fun greet name = "Hello, " ^ name ^ "!"

(* And this is string -> unit *)
fun printGreeting name = print (greet name ^ "\n")

You can check types in the REPL:

- print;
val it = fn : string -> unit

- "Hello";
val it = "Hello" : string

Common Beginner Mistakes

Forgetting the Semicolon

In source files, top-level expressions need semicolons:

1
2
3
4
5
(* Wrong - missing semicolon in source file *)
print "Hello, World!\n"

(* Right *)
print "Hello, World!\n";

Wrong String Quotes

1
2
3
4
5
6
7
(* Double quotes for strings *)
print "Hello, World!\n";

(* Single quotes are for characters (single char only) *)
val c = #"H"

(* NOT: print 'Hello'  -- Wrong! *)

Note: SML uses #"c" syntax for character literals, not just 'c'.

Forgetting the Newline

1
2
3
4
5
(* Without \n, output may not appear immediately *)
print "Hello, World!"    (* No newline *)

(* Better - include the newline *)
print "Hello, World!\n"  (* Has newline *)

Mixing Up = and ==

1
2
3
4
(* SML uses = for equality *)
val same = ("hello" = "hello")  (* true *)

(* There is no == operator in SML *)

Installing Standard ML Locally

If you prefer to run SML without Docker:

macOS with Homebrew

1
brew install smlnj

Ubuntu/Debian

1
apt install smlnj

From Source (SML/NJ)

Visit smlnj.org for installation instructions.

After installation:

1
2
3
4
5
# Run the file
sml hello.sml

# Or enter the REPL
sml

MLton (Compiled)

For compiled executables, use MLton:

1
2
3
4
5
6
# Install MLton
brew install mlton  # macOS

# Compile to native executable
mlton hello.sml
./hello

MLton produces highly optimized native code but requires a complete program (no REPL).

The REPL

SML has an excellent interactive environment. Enter sml at the command line:

Standard ML of New Jersey (64-bit) v110.99.3
-

- print "Hello, World!\n";
Hello, World!
val it = () : unit

- 1 + 2;
val it = 3 : int

- fun double x = x * 2;
val double = fn : int -> int

- double 21;
val it = 42 : int

- map double [1, 2, 3];
val it = [2, 4, 6] : int list

The - is the SML prompt. Commands:

  • Expressions are terminated with ;
  • Type Ctrl+D or OS.Process.exit OS.Process.success; to exit
  • use "file.sml"; to load a file

A Bit of History

Standard ML represents a pivotal moment in programming language design:

  • 1972 - Robin Milner creates ML for the LCF proof assistant at Stanford
  • 1978 - Luca Cardelli creates the first standalone ML compiler
  • 1983 - Milner begins standardizing ML
  • 1986 - SML/NJ development begins at Bell Labs
  • 1990 - The Definition of Standard ML published
  • 1997 - Revised Definition published

Standard ML’s formal definition influenced how we think about programming languages. Its type system, module system, and pattern matching appear in nearly every modern functional language.

Why Standard ML for Hello World?

Even in this simple program, you see SML’s philosophy:

  1. Simplicity - One line does the job
  2. Explicit Effects - unit return type makes side effects clear
  3. Type Safety - The compiler ensures print receives a string
  4. Functional Foundation - Functions are the primary building blocks
  5. No Boilerplate - No class definitions or main methods needed

Next Steps

Continue to the next tutorial to learn about Standard ML’s powerful type system and pattern matching.

Key Takeaways

  1. print outputs a string (add \n for newline)
  2. Function application uses space, not parentheses
  3. ; terminates top-level expressions in source files
  4. val () = ... is idiomatic for side-effecting expressions
  5. unit (written ()) is SML’s void type
  6. Type inference means you rarely need type annotations
  7. The REPL is great for experimentation

Running Today

All examples can be run using Docker:

docker pull eldesh/smlnj:latest
Last updated: