Hacker News new | ask | show | jobs
by vinceguidry 3829 days ago
HtDP changed the way I look at programming. But today I'm a diehard OOP fanatic. I want code that reflects the way I think, not code I have to work to understand.

I did not know but am not surprised to find that people write procedural Haskell. Procedural is how we all start out thinking about things. You do A, then B, to reach goal C. It takes time and experience and deliberate practice to improve upon that way of solving problems.

Functional moves the code in the direction of math. OOP moves in the direction of the domain. Math is harder to understand than domain logic, you will inevitably hack together an object system on top of your FP in order to implement domain logic.

Both styles have properties that are work better for certain domain concepts. What's nice about modern programming languages is that they build in primitives so you can use whichever style fits the concept you're fleshing out.

But trying to do everything functionally is ultimately counter-productive, in my opinion. It's the wrong format to declare high-level domain logic in, because high-level domain logic is that which is closest to human thought, not the underlying math.

Any FP 'architecture' will pretty much be object oriented. Math may treat state as ugly cruft, but humans need to group information close to where it's needed and in ways that make sense, that means state. Don't let the quest for mathematical beauty get in the way of solving your problem.

4 comments

I think your marriages of OOP to Domain Logic and FP to Math are incredibly off base. You can most definitely express domain logic in FP languages. The difference between FP and OOP to me is declarative and immutabe versus imperative and mutable. You could also have declarative OOP but in practice I've found most OOP code tends to be objects wrapping up procedures. But saying you can't express a domain properly in FP is laughable.
I would discuss the points you're making but your tone has me assuming that any attempt to do so would devolve into a fruitless argument. I'll just say that there's more than one way to look at the differences between programming paradigms.
I don't know why you would assume that.

> I'll just say that there's more than one way to look at the differences between programming paradigms.

That's actually what I'm saying and your original post is saying the exact opposite. I'm glad to see you've come around!

That's not what I said but whatever.
Most bugs I make are about state being out of sync - in fact every bug that is not because I misunderstood a problem is because state is out of sync.

In OOP programs, every object usually has it's own state, and it is updated all the time. If I can describe the world as one big hash-map, and my whole program can be while(true){writeToScreen(render(appState))}, then I'm very happy.

Haskell, which you seem to dislike, has a lot of syntax and some new concepts you need to pick up to get going. I recommend you to try out something that is extremely simple, like Clojure.

In Clojure there are not many things you need to learn before being dangerous. The data structures are lists '(), vectors [], hash-maps {} and sets #{}. There are functions (fn [] ) and macros, but you don't need them.

When coding functionally you can be sure you don't fuck something up by changing some state somewhere. And you don't need to mock data. And you don't need to do ORM. It's freaking great.

But I don't need Haskell or Clojure or anything like that to solve my problems. Ruby works just fine for me. I have lists, vectors, hash-maps and sets in Ruby. I can data pipeline simply and idiomatically. I don't work with math-heavy domains, so I don't need heavy math (type system) to write my programs.

When I have bugs in my code it's because I misunderstood the problem domain. State is missing somewhere, state that was unaccounted for and so is running loose in the code. The process of fixing the bug also illuminates what I was missing about the domain. Working on a program is the process of tightening the code around the domain.

> If I can describe the world as one big hash-map, and my whole program can be while(true){writeToScreen(render(appState))}, then I'm very happy.

I want my code to be flexible, and I only want to represent concepts once. I want to be able to interact with it on the command line, on the web, as an API. To do this I need to manage all the different ways other systems can get at the domain logic because otherwise I'm reimplementing parts of the system inside other systems. My program needs to be self-contained.

The best way I've seen to write and manage this kind of flexible code is with dynamic typing and OOP. Dynamic typing isn't a necessity, but it is a big help. OOP just makes everything sane.

"I don't work with math-heavy domains, so I don't need heavy math (type system) to write my programs."

I don't think you know what you're missing.

Type systems can be used to enforce pretty arbitrary things. Representation of data can be handled fairly well automatically, so types describing representation are only marginally useful (mostly where we care about interchange or care a lot about performance). However, if you make your types domain relevant, they can help you with domain relevant things. This can be simple - "I don't want to pass a bid price where I expected an ask price" - or it can be surprisingly sophisticated; I was able to ensure that certain actions were only taken on the correct threads, checked at compile time, in C - this helped me tremendously in refactoring when I discovered some piece of logic needed to live somewhere else. I really can't imagine doing that work without type checks, and it wasn't the slightest big "math heavy".

Can you explain what you mean by OOP? I personally think FP is a misnomer, it (as an architectural style) should be more properly called "algebra-oriented" programming (where algebra means general algebra and category theory). You basically design API based on some properties (equations) between objects.

If that's what you end up with, it's fine, but I don't think it should be called OOP, since this is not how OOP was understood in almost any OOP language (in particular, interfaces won't let you express equational relationships).

What I mean by OOP is representing domain concepts as objects with state. The objects merge interfaces (methods) with state. The objects and their relationships are the primary focus and the code is structured around them.

FP takes a data-first approach, making program flow the primary focus. State is, as much as possible, turned into data and otherwise squeezed out of the picture.

FP programmers go to lengths to eliminate state that I would consider excessive, only to recreate that state later when they have to start reasoning about the whole system.

As a diehard Rubyist, I find Ruby's semantics to be perfectly suited for any task of abstraction you want to throw at it. Just throw the errant code into a module. Turn it into a class when you start to need state.

I'll take an example from my current side project. I'm integrating the Pocket API into my app. I don't have time to rigorously implement a Pocket API consumer, but what I can do is implement the one API call that I need right now.

Instead of taking the time to do everything right the first time, I can simply make a single file with a module and call methods off of that module. As I implement more calls, I'll have a better idea of what kinds of state I'm managing, so I can refactor the code appropriately when the time is due.

It starts off procedural, code that just does things one after the other. It might acquire functional flavors as I work out the data pipeline. But eventually it will turn into proper objects with classes that can be passed around to whatever needs them. Things that would look like major architectural changes from the data pipeline point of view, are simple when you think about them as objects. Just implement the required interface and pass the instances to who needs them.

Just wondering, if you want code that reflects the way you think then how do you think about things and models that inherently don't map into objects?

I'm kind of the opposite: I rarely use objects (as in methods welded to holders of state) because they only fit in certain domains but when they do they are indispensable.

Hmm, interesting question. I guess I don't see how there are a great many things that couldn't be called objects. Particularly anything you can build a data structure around can be thought of as an object. So if your code has data to process, that data can be instantiated into objects.

But in this scenario, I would simply leave the code in an uninstantiated class, called a module in Ruby. I'd organize it as best I can with methods and perhaps submodules. My classes evolve organically as my application grows. Many of my classes start off as modules before I figure out what their state should look like.