
Algorithms as Objects - gilad
https://gieseanw.wordpress.com/2019/05/10/algorithms-as-objects
======
enriquto
I'm honestly impressed. After more than 20 years programming and reading about
programs, this is the first time that I find such a thing: a concise and well-
written description of the _exact opposite_ of everything that I believe in.
It's like the author was reading my mind and applied a NOT filter before
writing it back.

~~~
andyg_blog
I'm interested in learning more about your thoughts. Please elaborate?

~~~
Risord
Code smell #1/#2: If you have too long function/method break it apart. How
does this relate code being in function VS method

Code smell #3/#4: If you don't want to put helpers to global scope then just
don't. I don't see why closures are so bad. Top level function can act pretty
much same as class scope so what's the difference? I don't know about python
but if you really don't want to use closure another languages have other
encapsulation methods too.

Code smell #5: If you use closure you can have shared state. Passing state can
be noise but on the other hand if you only read/write "semi global" variables
it's messy too. If you need to pass lot amount of state it's usually hint that
you have some other problem in your code.

1\. Like said you don't need objects

2\. Function have single entry without ambiguity like @Someone shows.

3\. You should test behavior, not specific implementation. You don't need
mocking for pure functions.

4\. Is X optional? What about Y? Does order matter when calling 'withs'? And
that builder thing is directly stolen from all the Java jokes.

5\. I don't know what is Pythonish way to do delayed callability but I claim
that it should be independent thing rather than directly bounded to specific
function/object.

If it looks like a function, swims like a function, and quacks like a
function, then it probably is a function.

~~~
linkdd
5\. I do not know if it's pythonish, but I use and abuse functools.partial

------
vore
I don't think I agree with using objects as the public interface to an
algorithm, especially if its primary purpose is to apply some transformation
over the input (i.e. a natural fit for a function). The examples given have a
bit of a two-phase initialization smell: initialize some internal state, then
run the algorithm: steps that are never performed separately.

Objects as an algorithm internal state might be a better approach: the caller
doesn't have to worry about the algorithm's internal state, they only need to
call the external function entry point (and you'll destroy or otherwise forget
the internal state at the end of the function, rather than putting the burden
on the caller).

~~~
edflsafoiewq
A good example is hashing

    
    
        hasher = Hasher(iv)
        hasher.hash(data)
        hasher.hash(data2)
        ...
        hash = hasher.finish()
    

Compression/decompression is similar.

------
Someone
_”It’s impossible for a client to call the wrong function; there’s only one
way of doing things.

    
    
      my_algorithm = MyAlgorithmClass()
      results = my_algorithm.run()

“_

Only one? That API opens the door for

    
    
      my_algorithm = MyAlgorithmClass()
      results = my_algorithm.run()
      more_results = algorithm.run()
    

, which, hopefully, resets exactly the right amount of state to work just
fine.

There’s also the ‘fun’ of sharing an ‘algorithm’ between threads. That will
teach you that this ‘algorithm’ is more a set combining function arguments
with function state, and a method that uses them to compute a result. Next, if
you’re smart, you’ll separate the arguments from the state.

⇒ if you do something like this, use that builder pattern.

~~~
Phrodo_00
Worse, it opens the door for stuff like:

my_algorithm = MyAlgorithmClass() results = [0..100].parallelMap(lambda x:
my_algorithm.run())

~~~
Sharlin
Mandatory Rust evangelism: both of these examples would be forbidden at
compile time in a language with affine (or linear) types ;)

------
keymone
i really don't see how

    
    
        AlgorithmClass().WithXValue(10.0)
                        .WithYValue(20.0)
                        .WithEtc(...)
                        .Run();
    

is any better than

    
    
        Algorithm(x=10.0, y=20.0, etc=...)
    

the argument that text books teach us algorithms in form of single function
with inputs and outputs is quite weak, there's a very simple solution - break
up the large function into smaller functions.

i'll always prefer a set of isolated functions with explicit argument and
return declarations over a class implementation with methods because:

\- argument list tells me immediately what is required and what are possible
additional options as opposed to having to scan the method list for methods
matching `WithFooBar` names

\- methods become coupled to internal state of the algorithm object because
let's face it - code rots, maintainers take shortcuts, state proliferates,
everything that is allowed by a compiler/interpreter will eventually make it
into the codebase; it's much more preferable to avoid/diminish most of these
problems by writing isolated functions

\- explicit arg/return declarations help with default static analysis/checks,
much harder to do that with classes and internal state

~~~
agumonkey
Because it is not. It's oop distorsion field at its finest.

You can even find talks about why not do that (pyconf iirc).

Basically it's useless réification of functions as you state. But when you
love oop (and probably didnt use other things) well it's the ultimate itch to
scratch.

~~~
seanmcdirmid
That really is a straw man. There are plenty of OO enthusiasts who would
immediately call out fluent APIs as being very bad style.

Ideological demonization is pointless for advocates of either side.

~~~
agumonkey
I'm sorry, I've been burned too badly by the OO world (not helped by the fact
that I went in college at peak J2EE).

And to a certain extent.. I think this crowd still has issues. I spent time on
#java recently and I felt this acid confidence in OOP that stimulate their
mind that I really cannot fathom. I used to be like that, until I left this
field.

Now this is just personally, but of all the things I've read about, it was the
less useful for my brain. SQL did teach me some, pre OO Ada did teach me some,
ML taught me loads, Lisp worlds, forth etc etc yet OO falls flat to my eyes
99% of the time.

To be honest, when I read 'algorithms as objects' I upvoted, thinking it would
be distributed logic as object ala smalltalk. But again, fluent lipid layers..
And I think, the paradigm is to blame.

Now IIRC, you have a lot more knowledge about PL than most, so maybe you see
OO in a different light, a more rounded way.

~~~
seanmcdirmid
I’ve been burned by FP ideologues as well, I really don’t see a difference
between junior developers cargo culting X vs cargo culting Y.

------
hmschreck
I stopped reading here:

It’s fine for the algorithm to have separate logical parts, but in a function
this is a violation of the single responsibility principle.

If it's contained inside the algorithm, and never done elsewhere, then it's
still following the SRP to have the algorithm as a single function. This is
just someone who learned a new set of principles and over applies them.

~~~
andyg_blog
The SRP is a bit fuzzy when it comes to algorithms, though. What counts as
"one thing"? Bob Martin covers this in Clean Code. "If a function does only
those steps which are one level of abstraction below the stated name of the
function, then the function is doing one thing." SRP falls out of that, as
noted by Jeff Atwood([https://blog.codinghorror.com/curlys-law-do-one-
thing/](https://blog.codinghorror.com/curlys-law-do-one-thing/)).

The author is still overly vague here; it's not that the algorithm has
multiple logical parts, it's that it has those plus the kitchen sink.

------
smnplk
I sometimes wonder if people like Bob Martin aka "Uncle Bob" and Martin Fowler
have done more damage than good with their arbitrary rules. Or maybe people
are too blindly following their rules in every aspect of software design.

~~~
derefr
Fowler mostly just writes anthropologically about design patterns he
encounters, explaining how to implement them _given that_ you’ve decided to
implement them. He says a lot of “you’ve got to do X”, but in the context of
the article it means “evidence suggests that you’ve got to do X if you’re
doing Y; otherwise, what you’re doing won’t be recognized as Y and people will
get confused when you call it Y.” Fowler’s work is to software design-patterns
as [https://www.grammarphobia.com/blog](https://www.grammarphobia.com/blog) is
to English idioms: someone who provides analyses of what usages are accepted
and why, and what usages will get your text a bemused eyebrow-raise.

Robert C. Martin, meanwhile, is certainly paternalistic, but there’s a bit of
lost context to his writings: they’re targeted at the army of “Java school”
devs going directly from school to working at the big Java shops of the 90s.
His advice actually works quite well if your goal is to bootstrap yourself
from an education of “programming” but no “software engineering”, directly to
being able to leave a large pre-existing Java codebase “better than you found
it.” (His advice is no substitute for reading the code, absorbing the
architecture, and then extending it in natural and intuitive ways—but 90s Java
shops rarely gave their devs time to do that before moving them to the next
project.)

~~~
smnplk
Do not forget Fowlers twitter influences
[https://twitter.com/martinfowler/status/893100444507144192?l...](https://twitter.com/martinfowler/status/893100444507144192?lang=en)

------
viraptor
The traversal example is a bit weird. There's a simple solution to what the
author wanted. Replace `print(root.value)` with `yield root.value`. Now you've
got a simple algorithm where the caller can decide what to do with the values.
And it nicely resembles every kind of iterating things in python. Instead, the
object idea makes the algorithm specific to one single use case.

Same with the second example: You can write this as a filter on a function
which reads the data. You get a generic reader and a generic filter which can
be tested separately.

Twisting the code to fit it into the strategy pattern and preserving some
hidden state just to make that possible seems silly to me. Is the class really
worth writing instead of composable / reusable / testable / streamable:

    
    
        def data_reader(fin):
            fin.readline()
            for line in fin:
                yield [int(val) for val in line.split()]
    
        def fill_zero(source):
            last_day = 0
            for next_day, value in source:
                while last_day < next_day - 1:
                    yield 0
                    last_day += 1
                yield value
                last_day = next_day

~~~
edflsafoiewq
As far as the general principle goes, an iterator object is a great example of
"algorithms as objects" as opposed to "algorithms as monolithic functions".

~~~
viraptor
From the code simplicity POV they're very different though. Sure, an iterator
is an object here internally. But continue that way and you'll get to the
"objects are closures / closures are objects" discussion.

~~~
edflsafoiewq
I see the fact that Python has a nice syntax for writing generators as
incidental to the main point of "algorithms as objects". In Rust you'd
(currently) explicitly write a struct and its next() method, but its still the
exact same idea.

------
kace91
The start of the article is a very compelling description of why you should
split your code into smaller components instead of just having a single
structured procedure. Levels of abstraction, testability, etc. - Pretty basic
stuff I think.

I don't know where he took the leap to consider objects those components
though. You can perfectly expose a function that is internally split without
making it public in the public interface. And you certainly don't want to deal
with the extra problems of having shared state...

Object-oriented is not an antonym of monolithic. In fact, I'd say they tend to
end up being related terms, rather than opposed.

~~~
Sharlin
The justification for shared state that the author presents is that threading
a lot of (mutable) state as parameters through a tree of SRP-obeying internal
functions gets unwieldy. Factoring that state into the fields of a class and
making those functions methods is, of course, the ”obvious” solution when
you’re thinking inside OOP box.

It is also completely isomorphic to the obvious imperative solution of
factoring the state into a struct/record and passing that around. Both
approaches have the same problem of being based on mutable state.

The obvious solution to a _functional_ programmer is to make the state
immutable and make the functions _return_ the new state instead of modifying
it in place. Furthermore, each function should _only_ take the state it
actually needs. Of course, this doesn’t really help with the unwieldiness,
especially in a language without helpers such as the Clojure `->` threading
macro, or a reasonable lens library (and lenses themselves are not the most
obvious abstraction ever), so I guess YMMV. But getting rid of mutable state
is often a win anyway.

~~~
BubRoss
If state is immutable, then it's constant data, not state.

~~~
Sharlin
Au contraire! Languages with no mutation at all, like Haskell, still have a
well-defined concept of state. If anything, it's _more_ well-defined than in
imperative languages based on side effects. I refer you to Rich Hickey's
"Values and Change: Clojure’s approach to Identity and State" [1].

[1] [https://clojure.org/about/state](https://clojure.org/about/state)

~~~
BubRoss
Those are both just hiding mutation and copying.

------
UglycupRawky
Pendulum is slowly turning back to "Everything's an Object". Time to decorate
Jabba sections in that CV and remember to drop in an obvious reference to
GoF/Fowler/UncleBob writeups.

------
skybrian
This is a reasonable pattern commonly used to implement recursive-descent
parsers, where you have a parsing function for each language construct but
they all need to access common state (such as the lexer and the error output).
Passing this common state as separate parameters becomes tedious, so it makes
sense to group them into a struct.

It's not all that well-motivated in the article, though, since helper
functions aren't in themselves a code smell. You can just make them private.
It's also not strictly necessary to convert the helper functions into methods
on the object holding the state, though this can be convenient.

Also, the state object is an implementation detail and should usually be
private. The public API of of the parser can be a pure function.

------
Sutanreyu
[https://en.wikipedia.org/wiki/Strategy_pattern](https://en.wikipedia.org/wiki/Strategy_pattern)

------
andyg_blog
Although the examples are predominantly Python, the approach is useful in any
language where functions aren't forced to be in a class.

