A Modern Architecture for FP(degoes.net) |
A Modern Architecture for FP(degoes.net) |
AOP never really caught on because the advantages (keep logging and error handling separate from the business logic) were too small for the extra mess it added. Notably: difficult debugging, plus really weird COMEFROM-style behavior[0] if some well-meaning colleague added a little too much magic to the "aspects" (e.g. the automatically mixed-in logging code). The function suddenly does stuff that you never programmed. Good luck hunting that one down!
I strongly suspect this style of programming has similar downsides. Anyone tried it in large projects?
The main downside is complexity. It gets hard to reason about it fast. As types depend on more things, and grow bigger, error messages are hard to read, parenthesis abound, and people really start switching off. Type synonyms help here, but are not a panacea. (A similar problem for your "hard of debug" complaint.)
The COMEFROM style is mainstream on FP, so people are used and expect this kind of thing. That means that your functions better support doing stuff you never told it to do, because it will, you know it, and you'll adapt accordingly. It's normally not a problem, notice the "normally" there.
For example, array of tuples vs tuple of arrays. In pseudo Haskell notation:
:: ([a], [b], [c])
:: [(a, b, c)]
The second option enforces that you have the same number of a's, b's and c's. The second option can be faster to access in, say, C.
(I wonder whether you can do a similar, but more complicated, analysis for internal vs external linked lists and other containers.)
That kind of inefficiency is not practical for most real world programs!
To solve this problem, we can write an optimizing interpreter, which is a special kind of interpreter that can detect patterns and substitute them with semantically equivalent but faster alternatives.
But why?
...hmm, that's a rather big caveat arriving at the end of the article
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.
But in metaphorical (architectural) sense, yeah, you can probably get rid of it and use different monadic datatypes for different parts of the program that access different pieces in the world.
However, I am not clear how you then combine/serialize those different monads. It seems eventually you will get something like IO monad again. I am not sure but I think there needs to be some master monad going on in all Haskell programs that (potentially) serializes all the actions to the outside world.
There are better models of IO (some discussion here http://comonad.com/reader/2011/free-monads-for-less-3/) which essentially boil down to "bidirectional communication channel with a Real Machine" and these can provide a foothold for better semantics. Similar ideas drove the first "IO" based on input and output streams (but handles synchronization the way we need).
There's also chance for other ideas of RTS. For instance, we might embed 99% of Haskell into a real-time RTS and have real world interaction be governed by something like FRP.
I don't understand. This was posted yesterday (https://news.ycombinator.com/item?id=10806075) and you didn't even change the shortcut nobody outside domain could possibly know.
Are reposts ok?
If a story has had significant attention in the last year or so, we kill reposts as duplicates. If not, a small number of reposts is ok.
Normal thing to do would be understand that not everybody knows what "FP" is (free pascal, floating point?) and use full term in "corrected" repost. Perhaps original post didn't get any attention because people don't care about floating points.
Shortcuts are usefull when you mentioned term 100 times and it's just annoying, but you don't mention FP in title 100 times did you? There should be ban on using shortcuts on HN titles until it is well known shortcut (CPU, RAM, MB, ...)
Title should have enough information that if you stop random stranger on the street, can he understand or guess what is it about? The first thing rubber duck (https://en.wikipedia.org/wiki/Rubber_duck_debugging) would ask is: "what is FP?" and that means title is wrong and deserves zero comments.
Edit: another thing - title is only write once, but read hundred of thousands of times, you are literally wasting manhours here.
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.
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.
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).
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.
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.
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.
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".
universal : (f x -> x) -> (Initial f -> x)
and we can actually define Initial in this way universal' : Initial f -> (f x -> x) -> x
newtype Initial f = Initial { universal' :: forall x . (f x -> x) -> x }
In "strict Haskell" where we can act only finitely, we construct values of `Initial f` only by slapping finitely many layers of `f` on top of one another. For instance, when `f` is `data ConsF x = NilF | ConsF Int x` we can make a list [1, 2, 3] like Initial $ \join ->
let nil = join NilF
a : x = join (ConsF a x)
in 1 : 2 : 3 : nil
In other words, Initial things are described by their construction.Clearly, this relates directly to initial objects constructed via, e.g., Free, since it does roughly the same thing. Free emphasizes the notion of layering things atop one another. In Lazy Haskell we can still use this layering to construct non-initial objects (more to come below) but if Free were transported to Strict Haskell it would clearly only construct initial things.
---
So what about Finally Tagless?
class List l where
nil :: l
cons :: Int -> l -> l
We're still going to be constructing values of `List l => l` by application of finite layers! If anything, we're more stuck to this process now. cons 1 (cons 2 (cons 3 nil))) :: List l => l
If we swap this out for explicit dictionary passing we can see that we're missing an argument like data ListD l = ListD { nil :: l, cons :: Int -> l -> l }
\d -> cons d 1 (cons d 2 (cons d 3 nil)) :: ListD l -> l
and also that `ListD l` is equivalent to `ConsF l -> l`. It's really the same thing as the free method and is again operating initially.---
So what does it take to make something "final"? A final coalgebra of `f` would be an object `i : Final f -> f (Final f)` such that for any X and coalgebra `g : X -> f X` we have `universal g : X -> Final f` such that `i . universal g = fmap (universal g) . g` at type `X -> f (Final f)`. Or,
universal : (x -> f x) -> x -> Final f
which again can be used to define `Final f` data Final f where
Final :: (x, x -> f x) -> Final f
No longer can we define things by how they are constructed, now we must define them by how they are viewed. This opens up the doors to new kinds of structures, even in "Strict Haskell" natsFrom :: Int -> Final ConsF
natsFrom n0 = Final (n0, \n -> ConsF n (natsFrom (n + 1)))> 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!