7 min read
On this page

Basic Types and Inference

Haskell has a small set of primitive types and a famously powerful inference engine sitting on top. You can write a lot of Haskell without ever annotating a type — the compiler figures them out — but you should annotate top-level functions anyway, because the types are documentation and they pin down errors. This page is about the basic types you'll use every day, what type signatures look like, and how Hindley-Milner inference actually works.

Numbers: Int, Integer, Double, Float

Haskell's numeric types are split along two axes: integer vs floating-point, and machine-word vs arbitrary precision.

Int is a fixed-width signed integer, usually 64 bits on modern machines. It's fast, it overflows silently if you exceed its range, and it's what you reach for in tight loops. The minimum guarantee is at least 30 bits, so don't assume 64 bits in code that needs to be portable.

Integer is arbitrary-precision. There's no overflow — you can compute factorials with hundreds of digits and it just works. The cost is performance; arithmetic on Integer is slower than Int because it might allocate. For most application code this doesn't matter. For numeric code, it does.

Double is a 64-bit IEEE 754 floating-point number. It's what you use for almost all real-number computation.

Float is a 32-bit float. Rarely worth using in Haskell unless you're interfacing with C code or have a specific memory constraint.

ghci> :type 42
42 :: Num a => a
ghci> 42 :: Int
42
ghci> 42 :: Integer
42
ghci> 2 ^ 100 :: Integer
1267650600228229401496703205376
ghci> 2 ^ 100 :: Int
0  -- silent overflow on a 64-bit Int

Notice the type of 42 is Num a => a. Numeric literals are polymorphic — they take whichever numeric type the context requires. You only need to disambiguate when there's no context to pin them down.

For real-world projects, prefer Int over Integer unless you have arbitrary-precision needs. Most code never overflows in practice, and the performance difference is real.

Char and String

Char is a Unicode code point, written with single quotes: 'a', 'λ', '\n'. It's a full 32-bit code point, not just ASCII.

String is [Char] — literally a linked list of characters. This is convenient for pedagogy and pattern matching but terrible for performance. A million-character string is a million-link linked list, with all the overhead that implies.

ghci> :type 'a'
'a' :: Char
ghci> :type "hello"
"hello" :: String
ghci> :info String
type String = [Char]   -- so it's literally a list

In real code, almost nobody uses String. The library you reach for is Data.Text (or Data.Text.Lazy), which is a packed UTF-8 representation. For binary data, Data.ByteString. The pattern is:

import qualified Data.Text as T
import qualified Data.Text.IO as TIO

greeting :: T.Text
greeting = T.pack "Hello, world"

main :: IO ()
main = TIO.putStrLn greeting

Most cabal projects enable the OverloadedStrings extension so that string literals can be used as Text directly:

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.Text as T

name :: T.Text
name = "Ada Lovelace"   -- a Text, not a String, thanks to OverloadedStrings

Treat String as fine for tutorials and small scripts, and switch to Text when you're writing something real.

Bool

Two values, True and False. Operators are && (and), || (or), not. The standard logical functions are exactly what you'd expect.

ghci> True && False
False
ghci> not (5 > 3)
False
ghci> if True then "yes" else "no"
"yes"

Bool is just a normal data type — there's nothing magical about it, and we'll see how to define it ourselves in the next page on algebraic data types.

Type Signatures

A type signature uses :: (read "has type"). It can appear at top level or in expressions to disambiguate.

greeting :: String
greeting = "hello"

addOne :: Int -> Int
addOne x = x + 1

addTwo :: Int -> Int -> Int
addTwo x y = x + y

Function arrows associate right, so Int -> Int -> Int is Int -> (Int -> Int). This is currying — every Haskell function takes exactly one argument and returns either a value or another function. addTwo 3 is a perfectly valid expression with type Int -> Int. This is partial application, and it's used constantly.

add :: Int -> Int -> Int
add x y = x + y

addFive :: Int -> Int
addFive = add 5      -- partial application

ghci> addFive 10
15
ghci> map (add 100) [1, 2, 3]
[101, 102, 103]

The convention is to write a top-level signature for every function. The compiler will infer it for you, but writing it down anchors errors and serves as a contract. When you change a function and break a caller, GHC's error points to the signature you wrote, which makes the diagnostic dramatically clearer.

Type Inference: How Hindley-Milner Works

Haskell uses a type system descended from Hindley-Milner, an algorithm developed in the late 1970s. The big result of HM is that you can infer the most general type of any expression without annotations, in nearly linear time. This is why Haskell can give you full type checking with very little ceremony.

The intuition: every expression has a most general type, and the inference algorithm finds it by collecting constraints from how the expression is used and unifying them.

Take a function:

identity x = x

You didn't write a type. GHC reasons:

  1. identity takes one argument x and returns it.
  2. The argument has some type — call it a.
  3. The return value has the same type as x, which is a.
  4. There are no other constraints on a.

So identity :: a -> a. The type variable a is universally quantified — identity works for any type. This is parametric polymorphism, and it falls out of the inference.

Or:

addOne x = x + 1

GHC reasons:

  1. x + 1 — so x must be a number.
  2. 1 is polymorphic, but for + to work, both sides must have the same type.
  3. The constraint is Num ax has some numeric type.

So addOne :: Num a => a -> a. The Num a => is a typeclass constraint — it says a can be any type in the Num typeclass (Int, Integer, Double, etc).

This is why you can write polymorphic code without any <T> syntax. The polymorphism is the default; concrete types are a special case that emerges from constraints.

Polymorphic Types

Type variables are written in lowercase, distinguished from concrete types which start uppercase. Some common polymorphic functions and what their types tell you:

length :: [a] -> Int
-- "give me a list of any type, I'll give you its length"

head :: [a] -> a
-- "give me a list, I'll give you its first element"
-- (note: this is partial — crashes on empty lists; modern Haskell discourages it)

map :: (a -> b) -> [a] -> [b]
-- "give me a function from a to b and a list of a's, I'll give you a list of b's"

(.) :: (b -> c) -> (a -> b) -> a -> c
-- function composition: combine two functions into one

The type of map carries real information. Because a and b are universally quantified — they could be anything — map cannot inspect or modify the elements of the list. It can only call the function on each one. This property is called parametricity, and it means you can read a lot of behavior off the type alone. A function with type [a] -> [a] can only rearrange, drop, or duplicate elements; it can't fabricate new ones because it doesn't know what type a is.

This is the famous Wadler paper "Theorems for Free!" — you genuinely can derive theorems about a function purely from its type, in Haskell, because the type system is strong enough to prevent the function from cheating.

Type Variables in Practice

You'll see a, b, c as the generic placeholders — they don't mean anything specific. You'll also see longer names when they have semantic content:

lookup :: Eq k => k -> [(k, v)] -> Maybe v
-- k for key, v for value, with the constraint that k supports equality

The constraint Eq k => is a typeclass constraint — k must be a type with an == defined. We'll cover typeclasses in detail on a later page; for now, just read the constraint as "any type that supports equality."

Inference Has Limits

There are situations where GHC can't infer a type, or infers something different from what you wanted. Two cases come up most often.

Numeric literal defaulting. If you write let x = 42 in GHCi, GHC has to pick something — 42 :: Num a => a is too vague to evaluate. By default it picks Integer. This can produce confusing results when you mix types.

ghci> let x = 1 / 2
ghci> x
0.5     -- GHC defaulted to a fractional type
ghci> let y = 1 `div` 2 :: Int
ghci> y
0       -- integer division

Higher-rank types. When a polymorphic function is passed to another function, GHC sometimes can't figure out the right quantification without an explicit annotation. In practice this comes up rarely and usually means you need a forall somewhere.

The fix in both cases is to add a type annotation. Don't fight inference — when it can't figure something out, write the type.

Common Pitfalls

Using String for everything. It's slow. Switch to Text for any non-trivial work. Add OverloadedStrings to your project's default extensions and the migration cost is minimal.

Confusing Int and Integer. Most code wants Int. Use Integer only when you need arbitrary precision. The names are unfortunate — many people assume Integer is the default and Int is a fixed variant; in practice it's the other way around for performance.

Skipping top-level signatures. Inference works, but missing signatures make error messages worse and obscure your intent. Write them.

Reading => as ->. The fat arrow => is for typeclass constraints. The thin arrow -> is for function arguments. Eq a => a -> a -> Bool means "for any a that supports equality, take two as and return a Bool."

Trusting numeric defaulting. When something doesn't behave as expected, suspect that GHC inferred a different numeric type than you wanted. Check with :type in GHCi and add an annotation if needed.

Key Takeaways

Haskell's basic types are Int, Integer, Double, Char, String (which is really [Char]), and Bool. Use Text instead of String for real work. Type signatures use :: and arrows ->; multi-argument functions are curried. The Hindley-Milner inference engine derives the most general type of any expression, giving you parametric polymorphism for free. Type variables are lowercase, concrete types are uppercase. The => arrow introduces typeclass constraints. Always write top-level signatures even though inference would let you skip them.