Skip to content

Object Oriented Programming (OOP)

The Idea

OOP takes structured programming to the next level. Structured programming encapsulates local data in a function. The user does not need to know anything about the function other than the interface (inputs and outputs).

OOP recognizes that some groups of functions "hang together" because they operate on the same object. One idea is to group these functions together.

The second idea is that certain persistent data "belong to" an object. They should only be manipulated by functions that also "belong to" the object.

OOP therefore bundles data (called properties) and functions (called methods) together.

Example: Utility function

\(u(c,l) = c ^{(1-\sigma)} / (1-\sigma) + \phi \log(l)\)

Persistent data include: parameters (\(\sigma, \phi\)).

Methods include: compute \(u_{c}, u(c,l)\), inverse marginal utility, indifference curves.


There is nothing that OOP can do that could not be done without OOP. The benefits lie in code organization.

The programmer sees all methods that operate on the object in one place. That makes it easier to

  • test the code
  • modify the code
  • ensure consistency

Since all code is in one place, it is easy to swap out.

Imagine you want to compute a model with different utility functions. With OOP, all you need to do is swap out the utility function object. Ideally, the other code remains unchanged.


Some view OOP as misguided. The focus should be on algorithms or interfaces, not objects.

I don't think there is a right answer.

Some code "naturally" structures itself around objects.

Example: Utility functions, their parameters, and the obvious methods (computing utility, marginal utility, ...).

Other code "naturally" structures itself around algorithms. This is particularly true for "lower level" tasks, such as sorting.

OOP in Julia

Julia is not an OOP language. There is no inheritance.

But it is easy to create user defined types that store their persistent state and to define methods that "belong" to those types.

Example: Utility Function

struct CRRA{T}
  sigma :: T

utility(u :: CRRA{T}, c) where T = 
  (c .^ (one(T) - u.sigma)) ./ (one(T) - u.sigma);

marginal_utility(u :: CRRA{T}, c) where T = 
  c .^  (-u.sigma);
julia> u = CRRA(2.0)

julia> utility(u, [2.0, 3.0])
2-element Vector{Float64}:

Digression: parametric types

CRRA{T} is a parametric type.

T parameterizes the CRRA object.

This is more general than hard-wiring the type of sigma, but has the same performance.

Not annotating the type, as in

struct CRRA

would be slower (because Julia would not be able to specialize methods on the type of sigma).

Why do we care?

When we solve structural models, OOP is a natural approach.


julia> abstract type AbstractUtility{T} end

julia> struct CRRA{T} <: AbstractUtility{T}
           σ :: T

julia> abstract type AbstractProductionFunction{T} end

julia> struct CobbDouglas{T} <: AbstractProductionFunction{T}
           α :: T
           tfp :: T

julia> struct Model{T}
           util :: AbstractUtility{T}
           prodFct :: AbstractProductionFunction{T}

julia> m = Model(CRRA(2.0), CobbDouglas(0.3, 1.5))
Model{Float64}(CRRA{Float64}(2.0), CobbDouglas{Float64}(0.3, 1.5))

Now we change our mind and go for exponential utility:

julia> struct ExponentialUtil{T} <: AbstractUtility{T}
           μ :: T

julia> m = Model(ExponentialUtil(0.7), CobbDouglas(0.3, 1.5))
Model{Float64}(ExponentialUtil{Float64}(0.7), CobbDouglas{Float64}(0.3, 1.5))

As long as utility(u, c) is defined for each utility function, nothing in our code changes when we replace one utility function with another.

And the model automatically keeps track of the parameters that actually matter in this model version.

Models become like Legos.

Note: struct Model{T} is not efficient. We should have written

# Note the `T` in the parametric type definition
struct Model{T, U <: AbstractUtility{T}, F <: AbstractProdFct{T}}
    util :: U
    prodFct :: F

See notes on parametric types.


Matlab documentation on object oriented programming.