# The Type System

We've been using types since chapter three without ever looking directly at them. Time to fix that, because Julia's type system is where the language's two apparently contradictory promises — "easy like Python" and "fast like C" — turn out to be the same promise.

I'll warn you up front: this chapter and the next are the conceptual heart of the book. If something doesn't land immediately, that's expected. Read it, write some code, come back.

## The shape of the hierarchy

Every value in Julia has exactly one **concrete** type. Concrete types can be instantiated, have a defined memory layout, and have no subtypes:

```{code-block} julia
typeof(42)          # Int64
typeof(3.14)        # Float64
typeof("hello")     # String
```

Above them sit **abstract** types, which organize concrete types into a tree. Abstract types cannot be instantiated — you never hold a `Real` in your hand, you hold an `Int64` that *is a* `Real`. Their entire job is to let you write code that applies to a whole family at once.

```{code-block} julia
julia> Int64 <: Integer <: Real <: Number <: Any
true
```

`<:` reads "is a subtype of." A slice of the standard numeric tower looks like this:

```
Any
└─ Number
   └─ Real
      ├─ Integer
      │  ├─ Signed   → Int8, Int16, Int32, Int64, Int128, BigInt
      │  ├─ Unsigned → UInt8 … UInt128
      │  └─ Bool
      ├─ AbstractFloat → Float16, Float32, Float64, BigFloat
      ├─ Rational
      └─ AbstractIrrational
```

You can explore this yourself, which I'd encourage — it's more instructive than any diagram I can draw:

```{code-block} julia
supertype(Int64)         # Signed
subtypes(Integer)        # [Bool, Signed, Unsigned]
Float64 <: Real          # true
Float64 <: Integer       # false
```

`Any` sits at the top. Every type is a subtype of `Any`, and an unannotated function argument is implicitly `::Any`.

### Why "abstract" and "concrete" is the distinction that matters

Here's the practical payoff of that vocabulary, and it's worth stating bluntly:

- **Annotate function arguments with abstract types.** `f(x::Real)` accepts anything real, and Julia compiles a fast specialized version for each concrete type you actually pass.
- **Give struct fields and array elements concrete types.** `Vector{Float64}` is a contiguous block of floats. `Vector{Real}` is a block of *pointers* to floats scattered on the heap, because the compiler doesn't know how big a `Real` is.

Same word, opposite advice, depending on where it appears. Getting this backwards is the single most common performance mistake beginners make in Julia. Abstract in signatures, concrete in storage.

## Defining your own types

A `struct` groups related fields into a new type:

```{code-block} julia
struct LedgerEntry
    account::String
    amount::Float64
    posted::Bool
end
```

Julia gives you a constructor for free:

```{code-block} julia
julia> e = LedgerEntry("GL-4001", 1200.50, true)
LedgerEntry("GL-4001", 1200.5, true)

julia> e.account
"GL-4001"

julia> e.amount
1200.5
```

Because every field has a concrete type, Julia knows this struct's exact memory layout: 8 bytes for a pointer to the string, 8 for the float, 1 for the bool. An array of a million of these is a tight, contiguous block — the same thing a C `struct` array would give you.

If you leave fields unannotated:

```{code-block} julia
struct SlowEntry
    account
    amount
    posted
end
```

...it still works, but every field is `Any`, every field is a pointer, and you've thrown away the performance. Annotate your struct fields. This is the one place where being explicit about types is unambiguously worth it.

### Structs are immutable by default

```{code-block} julia
julia> e.amount = 999.0
ERROR: setfield!: immutable struct of type LedgerEntry cannot be changed
```

This is deliberate, and it's the right default. Immutable values can be stored inline in arrays, passed in registers, and freely shared without anyone worrying about who might modify them. They can't create the class of bug where a function you called quietly changed your data.

When you genuinely need mutation, ask for it:

```{code-block} julia
mutable struct Account
    id::String
    balance::Float64
end

a = Account("GL-4001", 1200.0)
a.balance += 500.0        # fine
```

Start immutable. Reach for `mutable struct` when you have a specific reason — an object with genuinely changing state over its lifetime, like a running accumulator or a connection handle. We'll dig into the consequences of mutability in its own chapter.

:::{note} Redefining structs no longer requires a restart
For years, one of Julia's genuine annoyances was that changing a `struct` definition meant restarting your entire session — which, given compile latency, was painful during development.

As of **Julia 1.12** this is fixed. Types can be redefined in a running session, thanks to work on the "world age" mechanism. Combined with `Revise.jl` (which auto-reloads your code as you edit it), the interactive development loop is dramatically better than it was on the 1.10 LTS. If you've bounced off Julia before because of this, it's worth another look.
:::

### Constructors

The default constructor takes all fields in order. You'll often want something friendlier — an **outer constructor** is just a function with the same name as the type:

```{code-block} julia
struct LedgerEntry
    account::String
    amount::Float64
    posted::Bool
end

# Convenience: entries default to unposted
LedgerEntry(account, amount) = LedgerEntry(account, amount, false)
```

When you need to **validate** before the object exists, use an **inner constructor**, which lives inside the struct and calls `new`:

```{code-block} julia
struct Percentage
    value::Float64

    function Percentage(v)
        0 <= v <= 100 || throw(ArgumentError("percentage must be in 0..100, got $v"))
        return new(v)
    end
end
```

```{code-block} julia
julia> Percentage(45.0)
Percentage(45.0)

julia> Percentage(150.0)
ERROR: ArgumentError: percentage must be in 0..100, got 150.0
```

Defining an inner constructor replaces the default one, which is exactly what you want here — now it's impossible to construct an invalid `Percentage` anywhere in your program. That's a strong guarantee for six lines of code.

## Parametric types

Here's where the type system gets genuinely powerful. A type can take another type as a parameter:

```{code-block} julia
struct Measurement{T<:Real}
    value::T
    uncertainty::T
end
```

`Measurement{Float64}` and `Measurement{Int}` are now two different concrete types, generated from one definition:

```{code-block} julia
julia> m = Measurement(9.81, 0.02)
Measurement{Float64}(9.81, 0.02)

julia> typeof(m)
Measurement{Float64}
```

The `T<:Real` constraint says "T can be any subtype of `Real`," so `Measurement{String}` is rejected at construction time.

You've been using parametric types all along without naming them. `Vector{Float64}`, `Dict{String, Int}`, `Rational{Int64}` — all the same mechanism. This is how Julia gets containers that are both generic (write `Vector` once) and fast (`Vector{Float64}` has a concrete layout).

### A subtlety worth knowing early

```{code-block} julia
Vector{Float64} <: Vector{Real}      # false!
```

This surprises everyone. `Float64 <: Real` is true, so shouldn't a vector of floats be a vector of reals? No — and for a good reason. If it were, you could take a `Vector{Float64}`, treat it as a `Vector{Real}`, and push a `BigInt` into it, breaking the memory layout the compiler is relying on.

When you want "a vector of any real type," write it like this:

```{code-block} julia
function total(xs::Vector{<:Real})
    ...
end
```

`Vector{<:Real}` means "a `Vector{T}` for some `T` that is a subtype of `Real`." That accepts `Vector{Float64}` and `Vector{Int}` while keeping each one's concrete layout intact.

Better still, in most cases, don't annotate at all — or annotate the abstract container:

```{code-block} julia
function total(xs::AbstractVector)
    ...
end
```

`AbstractVector` also accepts views, ranges, and the many specialized array types in the ecosystem. Writing against the abstract type is how your function ends up working with people's data structures you've never heard of.

## Defining your own abstract types

You can extend the hierarchy yourself:

```{code-block} julia
abstract type Instrument end

struct Bond <: Instrument
    face_value::Float64
    coupon::Float64
end

struct Equity <: Instrument
    shares::Int
    price::Float64
end
```

Now `Bond` and `Equity` are both `Instrument`s, and you can write functions that accept any `Instrument`:

```{code-block} julia
value(b::Bond) = b.face_value
value(e::Equity) = e.shares * e.price

portfolio_value(xs::Vector{<:Instrument}) = sum(value, xs)
```

Note what that abstract type does and doesn't do. It does **not** carry fields, methods, or implementation — Julia has no inheritance of data or behaviour. It is purely a name in the tree that says "these things belong to the same family and can be handled together."

That's a smaller feature than a base class in Java or Python, and deliberately so. What fills the gap — and what makes this design work — is the subject of the next chapter.

## Useful type introspection

Habits worth building for exploring unfamiliar code:

```{code-block} julia
typeof(x)              # the concrete type of a value
eltype(collection)     # the element type
fieldnames(LedgerEntry)   # (:account, :amount, :posted)
isa(x, Real)           # is x a Real?
isconcretetype(Float64)   # true
isbitstype(Float64)    # true — plain data, stored inline, no pointers
```

`isbitstype` is a nice one to know. A type is "bits" if it's plain immutable data with no references — which means arrays of it are dense, it can live in registers, and it can be sent to a GPU. `Float64` is. `LedgerEntry` isn't, because it contains a `String` (a pointer). That's not a problem, just a thing to be aware of when you're chasing performance.

## Try it yourself

1. Define an immutable `Invoice` struct with a customer name, an amount, and a due date (use a `String` for now). Construct one.
2. Add an outer constructor that defaults the due date to `"NET30"`.
3. Add an inner constructor that rejects negative amounts.
4. Make it parametric so the amount can be any `Real`, then check that `Invoice(…, 1200, …)` and `Invoice(…, 1200.0, …)` produce different concrete types.

### Solutions

```{code-block} julia
# 1–3 combined
struct Invoice
    customer::String
    amount::Float64
    due::String

    function Invoice(customer, amount, due)
        amount >= 0 || throw(ArgumentError("amount must be non-negative"))
        return new(customer, amount, due)
    end
end

Invoice(customer, amount) = Invoice(customer, amount, "NET30")

# 4.
struct PInvoice{T<:Real}
    customer::String
    amount::T
    due::String
end

typeof(PInvoice("Acme", 1200, "NET30"))     # PInvoice{Int64}
typeof(PInvoice("Acme", 1200.0, "NET30"))   # PInvoice{Float64}
```

You now know how Julia organizes types and how to add your own to the hierarchy. Next: how Julia decides *which* function to run — the idea everything else in the language is built around.
