Getting Started

Course 1 · Ch 1
Getting Started
main :: IO () — the type signature that carries this entire course's throughline

Every language on this site so far has been imperative or object-oriented in flavor — even the ones with functional features bolted on (Java's streams, C#'s LINQ) are still fundamentally "do this, then this" languages underneath. Haskell isn't. This course treats it as what it genuinely is: the real origin point for ideas already met elsewhere on this site under different names — Rust's Option/Result, pattern matching, even the whole idea of a trait. This first chapter states the throughline that runs through the entire track: in Haskell, a function's type doesn't just describe its inputs and outputs — it tells you whether that function can have side effects at all.

GHC and GHCi

$ ghci Prelude> 2 + 2 4 Prelude> :quit

GHC (the Glasgow Haskell Compiler) is the standard toolchain; GHCi is its interactive REPL — similar in spirit to Python's or Node's own REPLs, useful for quick experiments before committing to a real file.

Your First Program

main :: IO () main = putStrLn "Hello, World!"

Two lines: a type signature (main :: IO ()) and the actual definition (main = ...) below it. Here's the reveal this whole course is built around: IO () is not decoration or convention — it's a real, compiler-checked part of the type system, stating plainly that main is a computation which may perform IO and produces no meaningful value ((), pronounced "unit"). Every other language covered on this site writes a main with a signature that says nothing at all about what it does — void main(), static void Main(), no signature whatsoever in Python. Haskell's says it, in the type, from line one.

The Type Signature Convention

:: reads as "has type." GHC can infer nearly every type signature on its own — but by strong, near-universal community convention, every top-level function gets an explicit signature anyway, written directly above its definition. This is a real stated best practice, not busywork: it documents intent and catches a mismatch between what a function was meant to do and what it actually type-checks as, immediately, at the point of definition rather than somewhere downstream.

Laziness — Present From Line One

Prelude> take 3 [1..] [1,2,3] -- [1..] is an INFINITE list — nothing crashes or hangs

One small preview before Chapter 5's full treatment: [1..] genuinely describes an infinite list, and nothing about writing it crashes the program — Haskell only computes values as they're actually demanded, and take 3 only ever demands three of them. This is the language's default evaluation strategy, not a special trick, and it touches nearly everything from here on.

Running Programs — Compiled or Interpreted

$ ghc Main.hs -o hello # compiles to a real native binary $ ./hello $ runghc Main.hs # "interpreted" — but really compiled in memory first, then run

ghc compiles straight to native machine code — no bytecode, no VM, unlike Java's or C#'s own JIT-compiled-bytecode model. runghc feels interpreted for quick iteration, but it's genuinely compiling in memory first; Haskell is always compiled, never truly interpreted the way Python is.

Languagemain's signatureDoes it reveal IO capability?
Javapublic static void main(String[] args)no
C#static void Main()no
Pythonno signature at allno
Haskellmain :: IO ()yes — directly, in the type
Write explicit type signatures, even though GHC can infer them
A top-level type signature is cheap to write and expensive to skip — it documents intent, and a mismatch between the signature and the actual definition is caught immediately as a real compile error, rather than surfacing later as a confusing inferred type somewhere downstream.
Laziness means an error can hide in plain sight
Because nothing is evaluated until it's actually demanded, an error buried in a part of an expression that's never forced may simply never surface at all — a genuinely surprising first encounter for anyone used to eager, top-to-bottom execution. Chapter 5 covers this properly.

Coding Challenges

Challenge 1

Write a Main.hs with a main :: IO () that prints two separate lines of your choosing using two putStrLn calls, and run it with runghc.

📄 View solution
Challenge 2

In GHCi, evaluate take 5 [10..] and explain in a comment why this doesn't hang or crash despite [10..] describing an infinite list.

📄 View solution
Challenge 3

Write a short comment contrasting Java's public static void main(String[] args) against Haskell's main :: IO (), explaining specifically what information Haskell's version encodes that Java's does not.

📄 View solution

Chapter 1 Quick Reference

  • GHC compiles to native code directly; GHCi is the interactive REPL
  • main :: IO () — the type signature itself states this computation may perform IO, unlike any other language's own main signature
  • :: reads as "has type" — explicit top-level signatures are strong convention even though GHC can infer them
  • Haskell is lazy by default — [1..] is a real infinite list, safe because nothing is computed until demanded
  • ghc always compiles to native code; runghc feels interpreted but compiles in memory first
  • Next chapter: functions and types — Hindley-Milner inference and currying as the default