Live data from Hacker News

The compiler is your best friend

blog.daniel-beskin.com

71–80 of 149 posts

Re: The compiler is your best friend

#71

> A common pattern would be to separate pure business logic from data fetching/writing. So instead of intertwining database calls with computation, you split into three separate phases: fetch, compute, store (a tiny ETL). First fetch all the data you need from a database, then you pass it to a (pure) function that produces some output, then pass the output of the pure function to a store procedure. Does anyone have a…

> there's no way to "fetch all the data" up front. this is incorrect I assume there's more nuance and complexity as for why it feels like there's no way. Probably involving larger design decisions that feel difficult to unwind. But data collection, decisions, and actions can all be separated without much difficulty with some intent to do so. I would suggest caution, before implementating this directly: but imagine a…

That's a good point, thinking about it some more, I think the business logic feels so trivial that it would make the code harder to reason about if it were separated from the effects. Currently, I have one giant function that pulls data, filters it, conditionally pulls more data, and then maybe has one line of effectful code.

I could have one function that pulls the wallet balance for all users, and then passes it to a pure function that returns an object with flags for each user indicating what action to take. Then another function would execute the effects based on the returned flags (kind of like the example you gave of processing a pending charges table).

The value of that level of abstraction is less clear though. Maybe better testability? But it's hard to justify what would essentially be tripling the lines of code (one function to pull the data, one pure function to compute actions, one function to execute actions).

Additionally, there's a performance cost to pulling all relevant data, instead of being able to progressively filter the data in different ways depending on partial results (example: computing charges for all users at once and then passing it to a pure function that only bills customers whose billing date is today).

Would be great to see some more complex examples of "functional core imperative shell" to see what it looks like in real-world applications, since I'm guessing the refactoring I have in my head is a naive way to do it.

Re: The compiler is your best friend

#72

> How many times did you leave a comment on some branch of code stating "this CANNOT happen" and thrown an exception? Did you ever find yourself surprised when eventually it did happen? I know I did, since then I at least add some logs even if I think I'm sure that it really cannot happen. I'm not sure what the author expects the program to do when there's an internal logic error that has no known cause and no defini…

>Further down the article, the author suggests bubbling up the error with a result type, but you can only bubble it up so far before you have to get rid of it one way or another. Unless you bubble everything all the way to the top, but then you've just reinvented unchecked exceptions.

Not necessarily. Result types are explicit and require the function signature to be changed for them.

I would much prefer to see a call to foo()?; where it's explicit that it may bubble up from here, instead of a call to foo(); that may or may not throw an exception my way with no way of knowing.

Rust is absolutely not perfect with this though since any downstream function may panic!() without any indication from its function signature that it could do so.

Re: The compiler is your best friend

#73
post #27

Earlier quoted context omitted.

Heh, recently I had to fix a bug in some code that had one of these comments. Feels like a sign of bad code or laziness. Why make a path that should not happen? I can get it when it's on some while loop that should find something to return, but on a if else sequence it feels really wrong.

Strong disagree about laziness. If the dev is lazy they will not make a path for it. When they are not lazy they actually make a path and write a comment explaining why they think this is unreachable. Taking the time to write a comment is not a sign of laziness. It’s the complete opposite. You can debate whether the comment is detailed enough to convey why the dev thinks it’s unreachable, but it’s infinitely better t…

Laziness might or might not be involved in either path.

A stupid developer might not even contemplate that something could happen.

A smarter developer might contemplate that possibility, but discount it, by adding the path and comment.

Why is it discounted? Probably just priorities, more pressing things to do.

Re: The compiler is your best friend

#74

> How many times did you leave a comment on some branch of code stating "this CANNOT happen" and thrown an exception? Did you ever find yourself surprised when eventually it did happen? I know I did, since then I at least add some logs even if I think I'm sure that it really cannot happen. I'm not sure what the author expects the program to do when there's an internal logic error that has no known cause and no defini…

> At some level, the simplest thing to do is to give up and crash if things are no longer sane. The problem with this attitude (that many of my co-workers espouse) is that it can have serious consequences for both the user and your business. - The user may have unsaved data - Your software may gain a reputation of being crash-prone If a valid alternative is to halt normal operations and present an alert box to the us…

> Your software may gain a reputation of being crash-prone

Hopefully crashing on unexpected state rather than silently running on invalid state leads to more bugs being found and fixed during development and testing and less crash-prone software.

Re: The compiler is your best friend

#75
post #40

Earlier quoted context omitted.

Modern swift makes this technically possible but so cluttered that it's effectively impossible, especially compared with typescript. Swift distinguishes between inclusive and exclusive / exhaustive unions with enum vs protocols and provides no easy or simple way to bridge between the two. If you want to define something that typescript provides as easy as the vertical bar, you have to write an enum definition, a prot…

I think you’re conflating 'conciseness' with 'correctness.' The 'clutter' you're describing in Swift like, having to explicitly define an Enum instead of using a vertical bar |, is exactly what makes it more robust than TS for large-scale systems. In TypeScript a union like string | number is structural and convenient, but it lacks semantic meaning. In Swift, by defining an Enum, you give those states a name and a pu…

I mean there should be _some_ sugar in a language like swift, no? It has sugar in many parts of the language, being able to define a simple type union should not require custom declaration of every forwarding accessor, a protocol, an enum, bridging back and forth between them, etc.

It would be one thing to say that the vertical bar is a shortcut for these more exhaustive constructs (setting aside whether these constructs are actually exhaustive - they're not really for the purposes they're used for in practice), but as it is right now, you have no bar! If it were simple and easy to use you'd have enums dictate the state of every app which, as any app developer knows, is not how its currently done! Swift enums are so hard to use you have just a mess of optionals on view state instead of how it should formally be done, where almost every non-trivial view has a single state that's an enum of all possible configurations of valid states.

Indeed, if you put in a ton of effort to try and go down this route defining views "properly" with something like Swift TCA, you're not going to be able to get there even if you break up your app into 40 different targets because incremental recompilation will take a few minutes for something as simple as a color change.

Re: The compiler is your best friend

#76

> A common pattern would be to separate pure business logic from data fetching/writing. So instead of intertwining database calls with computation, you split into three separate phases: fetch, compute, store (a tiny ETL). First fetch all the data you need from a database, then you pass it to a (pure) function that produces some output, then pass the output of the pure function to a store procedure. Does anyone have a…

> Does anyone have any good resources on how to get better at doing "functional core imperative shell" style design?

I can recommend Grokking Simplicity by Eric Normand. https://www.manning.com/books/grokking-simplicity

Re: The compiler is your best friend

#77
post #16
post #13

Earlier quoted context omitted.

Better yet, `assert(false, message)`, with the message what you would have written in the comment.

`assert(false)` is pronounced "this can never happen." It's reasonable to add a comment with /why/ this can never happen, but if that's all the comment would have said, a message adds no value.

what language are we talking about? If it's cpp then the pronounciation depends on compiler flags (perhaps inferred from CMAKE_BUILD_TYPE)

Re: The compiler is your best friend

#78

> How many times did you leave a comment on some branch of code stating "this CANNOT happen" and thrown an exception? Did you ever find yourself surprised when eventually it did happen? I know I did, since then I at least add some logs even if I think I'm sure that it really cannot happen. I'm not sure what the author expects the program to do when there's an internal logic error that has no known cause and no defini…

> At some level, the simplest thing to do is to give up and crash if things are no longer sane. The problem with this attitude (that many of my co-workers espouse) is that it can have serious consequences for both the user and your business. - The user may have unsaved data - Your software may gain a reputation of being crash-prone If a valid alternative is to halt normal operations and present an alert box to the us…

> If a valid alternative is to halt normal operations and present an alert box to the user saying "internal error 573 occurred. please restart the app", then that is much preferred IMO.

You can do this in your panic or terminate handler. It's functionally the same error handling strategy, just with a different veneer painted over the top.

Re: The compiler is your best friend

#79

Earlier quoted context omitted.

> there's no way to "fetch all the data" up front. this is incorrect I assume there's more nuance and complexity as for why it feels like there's no way. Probably involving larger design decisions that feel difficult to unwind. But data collection, decisions, and actions can all be separated without much difficulty with some intent to do so. I would suggest caution, before implementating this directly: but imagine a…

That's a good point, thinking about it some more, I think the business logic feels so trivial that it would make the code harder to reason about if it were separated from the effects. Currently, I have one giant function that pulls data, filters it, conditionally pulls more data, and then maybe has one line of effectful code. I could have one function that pulls the wallet balance for all users, and then passes it to…

> The value of that level of abstraction is less clear though. Maybe better testability?

You wouldn't do it to make it easier to test; you would do it to make it easier to reason about. E.g. There's some bug where some users aren't getting charged. You already know where the bug is, or rather, you know it's not in the code that calculates what the price would be. But now, as a bonus, you also can freely modify the code that collects the people to charge, and don't have to worry if modifying that code will change how much other people get charged, (because these two code blocks can't interact with each other).

You know the joke/meme, 99 bugs in the code, take one down, patch it around, 104 bugs in the code? Yeah, that's talking about code like you're describing where everything is in one function, and everything depends on everything else as an intractable web somehow.

> But it's hard to justify what would essentially be tripling the lines of code (one function to pull the data, one pure function to compute actions, one function to execute actions).

This sounds like you're charging per line of source code. Not all code is equal. If you have 3x the amount of code, but it's written in a way that turns something difficult, or complex to understand and reason about, into something trivial to reason about, what you have is strictly better code.

The other examples or counter points you mention are merely implementation details, that only make sense in the context of your specific example/code base that I haven't read. So I'm gonna skip trying to reasoning about the solutions to them given the point of the style recommendations is to write code in a way that is 1) easier to reason about, or 2) impossible to get wrong ...but those really are the same thing

Re: The compiler is your best friend

#80

Earlier quoted context omitted.

> or have it done as part of the standard functions like `map`. Which are all well and good when they are applicable, which is not always 100% of the time. > Because I usually do checks against the length of the array And what do you have your code do if such "checks" fail? Throw an assertion error? Which is my whole point, I'm advocating in favor of sanity-check exceptions. Or does calling them "checks" instead of "…

A comment have no semantic value to the code. Having code that check for stuff is different from writing comments as they are executed by the machine. Not read by other humans.

Of course you should put down a real assertion when you have a condition that can be cheaply checked (or even an assert(false) when the language syntax dictates an unreachable path). I'm not trying to argue against that, and I don't think anyone else here is either.

I was mainly responding to TFA, which states "How many times did you leave a comment on some branch of code stating 'this CANNOT happen' and thrown an exception" (emphasis mine), i.e., an assertion error alongside the comment. The author argues that you should use error values rather than exceptions. But for such sanity checks, there's typically no useful way to handle such an error value.

Post reply on HN