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.
Benefits¶
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.
Drawbacks¶
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
end
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)
CRRA{Float64}(2.0)
julia> utility(u, [2.0, 3.0])
2-element Vector{Float64}:
-0.5
-0.3333333333333333
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
sigma
end
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.
Example:
julia> abstract type AbstractUtility{T} end
julia> struct CRRA{T} <: AbstractUtility{T}
σ :: T
end
julia> abstract type AbstractProductionFunction{T} end
julia> struct CobbDouglas{T} <: AbstractProductionFunction{T}
α :: T
tfp :: T
end
julia> struct Model{T}
util :: AbstractUtility{T}
prodFct :: AbstractProductionFunction{T}
end
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
end
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
end
See notes on parametric types.
References¶
Matlab documentation on object oriented programming.