NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
Component Simplicity (jerf.org)
zackmorris 48 days ago [-]
An insight that might help tie functional programming (FP) and imperative programming (IP) together is that FP is spreadsheets and IP is macros (edit: recordings of human interaction and shell scripts). Ideally all business logic should be FP and event-driven interfaces are usually IP. I think of these as clusters of similar concepts:

  FP
  synchronous blocking
  immutable
  process isolation
  auto optimization
  auto parallelization
  higher-order methods
  scatter-gather, fork-join
  deterministic/repeatable (sometimes) reversable execution
  models and views in MVC
  ugly prefix/postfix syntax that doesn't look like algebra (not always)
  
  IP
  asynchronous nonblocking
  mutable
  shared state
  manual optimization
  manual parallelization
  generators/iterators
  threads, locks/mutexes
  nondeterministic promises/futures requiring snapshots to reverse execution
  controllers in MVC
  pretty infix syntax that looks like algebra (not always)
I believe that ideally we'd write everything in FP with no IP whatsoever. So no mutable variables, no pass by reference, no iteration. Just higher-order methods and copy-on-write, with static analysis simplifying intermediate code into its fastest and/or most concise representation to run in parallel as fast as possible on multicore CPUs. Each component runs single-shot using analogs of STDIN/STDOUT/STDERR. These can fully represent models and views functionally and even declaratively.

Then IP would be used to wire up components, similarly to controllers in MVP. For example when a UNIX executable blocks waiting for more data on an input stream, or more room in an output stream. The runtime/filesystem/network/OS between isolated executables is imperative.

FP and IP can be unified by enforcing immutability everywhere in an IP language, then transpiling the code to reshape it between prefix/infix/postfix notation. Unfortunately I/O still can't be implemented without monads, making this psuedo-FP language impure. So there may be no way to build an entirely pure FP runtime that can respond to dynamic input/output and exceptional behavior. It would be like having a spreadsheet with no connection to the outside world, where the values of cells can't be edited, so formulas could run but they never have reason to do so.

Attempts can also be made to eliminate IP altogether by providing impure functionality in FP languages by introducing monads for I/O and exceptional behavior, allowing mutation by isolating it in blocks, etc. I believe that makes impure FP languages equivalent to IP, corrupting them and defeating the point of using FP. Sometimes that makes sense though if/when we can live with the tradeoffs, or if we're porting code from IP languages that's too difficult to refactor to pure FP. Unfortunately most impure FP languages are also difficult to read by humans, limiting their adoption.

Without the basic understanding listed here, the world seems to naturally head towards IP, since it most closely mimicks how humans navigate the world. IP languages tend to borrow FP concepts like monads to implement async behavior like generators, futures and promises. This sacrifices determinism and expands shared state to such a degree that IP programs can only grow to a certain size before they are intractable. This can sort of be avoided with reducers like Redux or writelog databases, usually at a cost in having to maintain boilerplate.

The world also tends to double down on static types, categories, templates/generics, formal contracts/interfaces, etc, borrowed from FP without a clear understanding of when or why they're necessary. When often programs can be written much more concisely and clearly by avoiding custom types altogether and focusing on data-driven standard types like numbers, strings and JSON run through transformation functions. An obvious example being how Java programs are often bloated and overengineered because they are object-oriented instead of functional. It's amazing to see how so much boilerplate often contains so little actual business logic. And how languages inspired by Java, like C#, often seem to repeat its shortcomings.

I think of all of this as analogous to how beginners tend to implement a game's main loop with state machines for each sprite. Whereas in Unity they can use coroutines. Once we see how straightforward it is to write business logic in a one-shot fashion, it's hard to imagine going back to juggling the complexities of state machines. Especially since state machines and coroutines can be made equivalent via generators.

Another example is that digital electronic circuits are equivalent to a spreadsheet and can be represented and analyzed by FP, but it's hard to convert an IP program description into a circuit or analyze its behavior formally using techniques from logic.

Once we have these kinds of insights, it's difficult to unsee them. And to look at best practices without asking ourselves why we're doing things the hard way.

As far as I know, the only language that approximates pure FP with IP glue handled by the runtime to avoid monads is ClojureScript. Admittedly, I'm probably misunderstanding how it works.

ryandv 48 days ago [-]
> I believe that ideally we'd write everything in FP with no IP whatsoever. So no mutable variables, no pass by reference, no iteration.

> Then IP would be used to wire up components, similarly to controllers in MVP.

This viewpoint was popularized by Gary Bernhardt a while ago as "functional core, imperative shell." [0] Functional paradigms should be used for expressing core logic and transformation of data; code written in this style is side-effect free, "pure," rapidly unit testable in isolation, and even could in principle be executed in one's head, because it's the final expression value that is the goal when working in this paradigm.

Imperative paradigms on the other hand are not interested as much with expressions and values, but rather statement side-effects, behaviors, and the sequencing together of computations (and it's the concept of sequencing together effectful computations that monads attempt to abstract over). It is the glue code that ties together your pure, effect-free logic and embeds those expressions in a context where they actually have some interaction with computing, hardware, and the outside world; otherwise, they would just be formulae sitting in a spreadsheet with no connection to anything whatsoever.

> Attempts can also be made to eliminate IP altogether by providing impure functionality in FP languages by introducing monads for I/O and exceptional behavior, allowing mutation by isolating it in blocks, etc. I believe that makes impure FP languages equivalent to IP, corrupting them and defeating the point of using FP.

This point is interesting. One could argue that monads are in fact a way of preserving purity in FP while still attaining effectful imperative-like behavior, since the monad is "just" an AST or a pure value that describes a sequence of computations that ought to take place, and it's up to the runtime to actually interpret this data structure and execute or realize the effects merely described by the monadic value (I suppose an OOP design patternist would see similarities to a "Command pattern" here); however, Conal Elliot has taken this logic to the conclusion that, if it were the case, then the C language must also be "functionally pure." [1]

> When often programs can be written much more concisely and clearly by avoiding custom types altogether and focusing on data-driven standard types like numbers, strings and JSON run through transformation functions. An obvious example being how Java programs are often bloated and overengineered because they are object-oriented instead of functional. It's amazing to see how so much boilerplate often contains so little actual business logic. And how languages inspired by Java, like C#, often seem to repeat its shortcomings.

Maybe, but there is also a risk of running into the "Primitive Obsession" code smell [2]. If I have a URL, representing that URL as a String now allows malformed URLs or things that are not URLs at all to inhabit that type, reducing the number of static guarantees I can avail myself of; moreover, if I want to extract the scheme, path, etc. from the URL I now have to extract those attributes by way of lower-level text manipulation, substrings, and slices, instead of being able to simply read off those components of the URL from a higher-level representation that actually reifies those components as first-class attributes of a more structured data type.

I find that one of the main differences between Haskell and other languages and their ecosystems is the focus on finding good abstractions. Often I have seen Java or C# or golang interfaces two dozen methods wide. Not only does this break "interface segregation principle" and other SOLID dogmas, making it difficult to write other implementors (and often you just end up with the single implementor, at which point the interface barely abstracts over anything at all), such an overspecified interface tends to generalize poorly to other use cases.

The more you add to your interface, the more you specify and constrain its structure, the less general it becomes; hence TFA's celebration of monads which, having a minimal complete definition of one operator, can be applied to a shockingly wide variety of domains and are widely relied on by the ecosystem because, as an abstraction only a method wide, monads are subject to much less churn or interface breakage over time.

Finding good abstractions is hard, and IMO one of the central problems of writing software. I would summarize the article as observing that in imperative programming, it's possible to proceed naively but expeditiously, papering over the lack of strong and consistent abstractions by just throwing more glue code and flex tape at the problem to paper over any leaks. In Haskell, especially when first designing your types and data structures, the work of abstracting the problem domain adequately is front-loaded and you are forced to think about it up front.

[0] https://www.destroyallsoftware.com/screencasts/catalog/funct...

[1] http://conal.net/blog/posts/the-c-language-is-purely-functio...

[2] https://wiki.c2.com/?PrimitiveObsession

zackmorris 46 days ago [-]
Thanks for taking the time to write this, it feels validating because what I said came from the school of hard knocks! I agree with everything you stated. And also very much appreciate the insight about the anti-pattern of using primitives to represent domain ideas. I hadn't considered that, because most of my day jobs have been about getting anything at all to work and putting out fires.

I didn't know about Gary Bernhardt or functional core, imperative shell. Here are some more breadcrumbs I found:

https://github.com/kbilsted/Functional-core-imperative-shell...

http://www.javiercasas.com/articles/functional-programming-p...

https://news.ycombinator.com/item?id=18043058

https://news.ycombinator.com/item?id=34860164

https://news.ycombinator.com/item?id=12604767

I think of monads and promises/futures like imaginary numbers. We use them as placeholders, but they're difficult to reason about without executing them, which defeats most of the point of functional programming. That our goal is to connect inputs and outputs by way of some transformation that lets us treat the input-output pair and the computation as equivalent for substitution. Which opens up a host of possibilities around automatic optimization, parallelization and memoization (caching) which imperative programming mostly denies us.

I believe that the world's reluctance to embrace this stuff has quasi-political undertones similar to academics vs application in the real world.

In FP, I've noticed a certain dismissal of beauty/elegance as something subjective where it's assumed that developers can just simplify everything in their heads like in mathematics. The form of the solution doesn't really matter as long as it's functionally equivalent to another. Which tends to cause steep learning curves and a fondness for lingo. It's almost as if development time is meta and irrelevant, like developers have unlimited time budget to fully flesh out the domain solution, and the design scope will never change.

Whereas in IP, there's this sense that code doesn't matter, only the user experience does. The backlog is infinite, there is always zero time until deadline so developers are always behind, and design scope is a moving target. Attempts to formally architect the domain solution are viewed as over-engineering, or waterfall instead of agile. Readability, comments and documentation often dictate the success of projects more than shrewd architecting.

I would say 50-90% of my workload today is fixing other developers' unforced errors. I'm concerned mainly with how future-proof the code is. Which usually comes down to some very simple constraints which I can't apply because they aren't available. Like, most IP languages don't have const, or don't have it for data structures. Or constness is an add-on that often breaks the caller like in C++, and isn't on by default. Or keywords like final exist in the language because the compiler doesn't manage the arrangement of fields in data structures. Or multiple inheritance doesn't exist for similar reasons, putting the work of dealing with the limitations of interfaces and traits onto developers.

Anymore, I feel like pretty much any IP language creates more work for me than if I had just created the app in something like HyperCard, Filemaker, Microsoft Access, a spreadsheet, or even a database with stored procedures:

https://sive.rs/pg

Where I'm going with this is that I don't quite honestly know how much longer I can keep doing this. Programming has become a penance. Nobody in power seems to care about fixing the status quo. If anything, the complexities of "modern" software engineering create barriers to entry that benefit established players.

So even though I know just exactly what an FP core, IP shell language would look like and how it would be tested with behavior-driven development (BDD), I may never have the time to build it, so it may as well never exist. To preserve sanity, it's often safer to pretend that better ways are a fantasy, and just keep focusing on survival. Hope is a dangerous thing, hope can drive a man insane, as Red said in Shawshank Redemption.

So instead I evangelize and influence, and wait for the world to manifest it. Which hasn't happened in the 25 years since I got serious about disruption. If anything, it's all gone the opposite direction from what I had hoped for.

It's looking like we'll end up with imperative core, AI shell. Which will bring about the end of (profitable) human software engineering as we know it, when code becomes too complex for us to reason about, but approaches free. Which is looking more inevitable with each passing day. And really has me questioning why I got into programming in the first place. I thought I'd have more time.

tantalor 49 days ago [-]
> Imperative code often devolves into writing things that exponentially expand the state space of your program and hoping you can find a happy path through your newly-expanded space without wrecking up too many of the other happy paths

There's a good reason for this: it is the only way to run a business where the needs of customers and management changes on a constant basis.

> Functional programming generally involves slicing away the state space until it is just what you need.

This may be great idea for academics, but this approach is contraindicated in the field of software engineering, where the preferred solution is not the one with the "smallest state space", but the one that can deliver the most business impact with the least amount of effort. If I have to re-architect the entire program every Monday, that's not going to make leadership very happy.

HPsquared 49 days ago [-]
The most widely-used functional programming environment in the world - Excel - is very adaptable and used in business.

FP lets you add things freely without worrying about side effects.

account-5 49 days ago [-]
And likely the cause of a lot of business errors too. But at least it's not like parts of the human genome needed renamed because of it... Wait...
48 days ago [-]
naasking 49 days ago [-]
> If I have to re-architect the entire program every Monday, that's not going to make leadership very happy.

Good thing FP doesn't require that then.

eyelidlessness 48 days ago [-]
I think a more objective analysis would admit that it does require this, albeit sometimes not every Monday. And the article makes exactly this concession toward the end, to which I suspect GP’s comment was a glib reference.

The objective counterpoint to this glib interpretation IS NOT that, in FP, one never has to rearchitect the logic narrowing a program’s state space. Instead, an objective counterpoint might be more along the lines of this:

Yes, sometimes business rules evolve in a way that diverges from business logic. Yes, in a codebase that narrows its state space according to the prior business rules, that evolution may require revision to how state space is narrowed. But once that’s done, it is done. It doesn’t require adjusting dozens of special cases incidental to the previous business rules’ implementation: it only requires adjusting the special cases inherent to the business rules.

That’s objective. And it’s also objective to say that whether it’s compelling will depend on the business’s tolerance for that objectivity (or in lucky cases: its embrace of same).

prerok 48 days ago [-]
I mean, isn't the problem you are mentioning because of the functionality that is assuming too much? In the sense of functions that are behemots of assumptions?

It's actually easier if those functions are broken down into smaller pieces so when requirements change, you don't have to rearchitect: you just recombine them differently.

And I am not speaking academically but practically: have learned the hard way to not produce the behemots but to combine functions to the desired effect in business setting(s).

monooso 49 days ago [-]
> There's a good reason for this: _it is the only way_ to run a business where the needs of customers and management changes on a constant basis.

(Emphasis mine)

And yet there are businesses which run on Haskell.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 07:08:56 GMT+0000 (UTC) with Wasmer Edge.