As a former OOP guru, I can promise you that, if you commit to learning and using the Julian (and more broadly, functional) approach to programming, you will undoubtedly “see the light” eventually It truly is a more powerful paradigm, and the separation of data from behavior eliminates a whole range of anti-patterns that are endemic to OOP (like god classes, fragile base classes, ambiguous inheritance trees, etc).
Rather than forcing your code into rigid type hierarchies where each node represents some collection of data which “does” some number of things, you think about data and functions as separate things (which they are). A struct is just some collection of data with some type information, and a method is a function which can be defined on certain data patterns based on this type information.
Let’s consider a simple example where we make a struct to store the results of a (simple) linear fit:
abstract type AbstractLinearFit end
struct LinearFit{S,I} <: AbstractLinearFit
slope::S
intercept::I
end
slope(fit::AbstractLinearFit) = fit.slope
intercept(fit::AbstractLinearFit) = fit.intercept
predict(fit::AbstractLinearFit, x) = slope(fit)*x + intercept(fit)
Note the use of the getter methods slope
and intercept
in predict
(a pattern which should be familiar from OOP) which makes predict
totally agnostic to the underlying data structure of the AbstractLinearFit
that it is given. The default implementations assume there is a corresponding field defined on the type, but this behavior can be easily extended. Imagine, for example, an alternative parameterization of a linear fit which computes y = slope*(x + shift)
, i.e. where the intercept is actually slope*shift
. This could be implemented by simply defining a new type and a new dispatch for intercept
:
struct ShiftedLinearFit{F,T} <: AbstractLinearFit
slope::S
shift::T
end
intercept(fit::ShiftedLinearFit) = slope(fit)*fit.shift
This is admittedly a slightly contrived example, but the point is that we can achieve encapsulation also using functional patterns and multiple dispatch by defining method interfaces that are agnostic to data structure and then implementing that interface for various data types.
The only real loss from single-dispatch OOP is the partial loss of discoverability mentioned earlier in this thread. But I say partial because, even in OOP, objects do not necessarily contain every single function or method which can operate on them.