Live data from Hacker News

Functional Programming Self-Affirmations

norikitech.com

71–80 of 119 posts

Re: Functional Programming Self-Affirmations

#71
post #54

In many (but not all) scenarios "Make illegal states unrepresentable" is way too expensive to implement. Especially when dealing with a fast changing domain, having to support different versions of data shapes across long time periods: dynamic data definitions are more economic and will still provide sufficient runtime protection. "Errors as values" - what is an error? I see this pattern misused often, because not en…

""Make illegal states unrepresentable" is way too expensive to implement."

This has not been my experience. The speed increase in development not having to worry about the unrepresentable cases have been very valuable. In addition as requirements change migrating old data hasn't been a huge concern. For code changes refactoring the types helps address new cases as well.

Re: Functional Programming Self-Affirmations

#72
post #44
post #41

FP nerd: The pure core is nice and composable, with the imperative shell at the boundary. State Skeptic: Yes, But! How do you compose the 'pure core + impure shell' pieces? FPN: Obviously, you compose the pure pieces separately. Your app can be built using libraries built from libraries.... And, then build the imperative shell separately. My take is that the above solution is not so easy. (atleast to me!) (and not ea…

FP is not a silver bullet. GUI is the classic OOP showcase. > Ideally, you should be able compose them several of them into a single app and not have a custom implementation of a giant state If you are suggesting that components store their state, I'm not sure about "ideal" there. That works well for small GUI applications. In GUI applications of modest size, you do want a separate, well-organized and non-redundant d…

To your main point, I wouldn't say exactly that the component stores the state. But, rather that every component provides an initial value, possible events, and a default event handler which is a function from value to value. In effect, this is partially 'storing local state', but the above pieces can be composed to create a container component.

Note that there is no option really - the app wont be reimplementing how a key is handled in a text box. But composability means that the same principle should hold not just for OS/browser components but also for higher level components (A custom map or a tree-view where there are restrictions on types and number of nodes - these should also have default handling and delegation to upper levels.)

The global store choice makes it harder to have component libraries. But, the composable alternative has its problems too - redundancy and communication which skips layers (which requires 'context' in React).

Re: Functional Programming Self-Affirmations

#73
post #65
post #44

Earlier quoted context omitted.

FP is not a silver bullet. GUI is the classic OOP showcase. > Ideally, you should be able compose them several of them into a single app and not have a custom implementation of a giant state If you are suggesting that components store their state, I'm not sure about "ideal" there. That works well for small GUI applications. In GUI applications of modest size, you do want a separate, well-organized and non-redundant d…

This is a digression, but regarding OOP, my somewhat provocative view, is that it is not a natural thing, but in most languages, it is atleast 4 different concepts 1. Encapsulation/Namespace, 2. Polymorphism, 3. Extensibility(Inheritance is a special case) 4.Mutability. These four concepts are forced/complected into a 'class' construct, but they need not be. In particular, FP only varies on 4, but languages like ML,C…

Interesting link, thanks.

Re: Functional Programming Self-Affirmations

#74

> Errors as values > To me, this simply makes more sense: isn’t it objectively better to get a finite and predictable error value from a function than an unspecified exception that may or may not happen that you still have to guard against? Whether an error is returned as a value or thrown is orthogonal to whether it is finite and predictable. Java has checked exceptions. In Swift you also can specify the exceptions…

I agree with regards to checked exceptions. Unfortunately Java doesn't support any form of polymorphism over thrown exceptions so it makes your code much harder to reuse. In languages that support polymorphic effects I imagine this is less of a concern.

Re: Functional Programming Self-Affirmations

#75

Earlier quoted context omitted.

> How can you do "Errors as values" at a large scale without do-notation / monads? You don't need monads for this. You just need the ability to encode your error in some term like `Either e a` and ability to eliminate those terms. In Rust for example that is the `Result` type and you use pattern matching to eliminate those terms.

Then you end up with a pyramid of doom. Fine for small examples but it doesn't scale up easily. Rust has special syntax (`?`) for this AFAICT.

You still don't need monads for any of this. Monads give you an ad-hoc polymorphic way of doing monadic actions.

Short circuiting on `Either` is a specific case of this. You can define your own EitherBind in any language.

    eitherBind :: Either e a -> (a -> Either e b) -> Either e b
    eitherBind (Left e) _ = Left e
    eitherBind (Right a) f = f a
Now you can bind over Either to your heart's content without needing an encoding of Monads in your language.

Re: Functional Programming Self-Affirmations

#76
post #73
post #65

Earlier quoted context omitted.

This is a digression, but regarding OOP, my somewhat provocative view, is that it is not a natural thing, but in most languages, it is atleast 4 different concepts 1. Encapsulation/Namespace, 2. Polymorphism, 3. Extensibility(Inheritance is a special case) 4.Mutability. These four concepts are forced/complected into a 'class' construct, but they need not be. In particular, FP only varies on 4, but languages like ML,C…

Interesting link, thanks.

This page (https://reasonml.github.io/docs/en/module) is useful to see how a FP language can do what he wants. Because we have functors, which are functions from a group of modules/classes to another module/class, we can have Composition, Inheritance(single/multiple), Mixins etc.

Re: Functional Programming Self-Affirmations

#77

Earlier quoted context omitted.

Imagine you have a program where there are 4 options, W, X, Y, Z. Y and Z can't be set at the same time, and X can't be set unless W is set. If Y is set then X must be set as well. How do you represent this in a way that makes it impossible, even through programmer error elsewhere in the program, to have the flags in an invalid state? You can create en enum that looks like: enum program_state = ( W_X_Y_NZ, W_NX_NY_Z,…

Maybe I'm spoiled by TypeScript but this is how I'd do it in a structural typing system: type ConstrainedByW = | { w: false, x: false } | { w: true, x: bool } type ConstrainedByY = | { x: bool, y: false, z: bool } | { x: true, y: true, z: false } type ProgramState = ConstrainedByW & ConstrainedByY

Yes, but certainly you can see how even a toy example with just 4 booleans is already getting complicated?

Re: Functional Programming Self-Affirmations

#78

> Make illegal states unrepresentable This is a nice ideal to shoot for, but strict adherence as advocated in the article is a short path to algorithmic explosions and unusable interfaces on real life systems. For example, if you have two options that are mutually incompatible, this principle says you don't make them booleans, but instead a strict enum type populated with only legal combinations of the options. A gre…

Where does this exponentional size requirement come from?

Approximate expansion of the original claim, without direct endorsement:

Suppose you have an Order object that needs to track where some larger process is in relation to three subtasks. We could imagine say that the Inventory department needs to set a physical object aside, then the Billing department needs to successfully charge for it, then the Shipping department needs to retrieve that physical object and ship it.

You start from a description of this as "one Order contains three Subtasks" where a Subtask contains the Go-style (Optional[Result], Optional[Error]) type. This architecture almost fits into a relational database, except that foreign key constraints are a bit funky if you shove everything into one nullable Result column. But let's just have the Result be some random JSON in the Subtasks table and let our relational purists weep.

Then you read this advice and you start to see that this allows for a lot of illegal states: things could contain both a result AND an error, or neither. You eventually decide that neither, is an allowed state. These are two boolean flags representing only 3 legal states and so they need to be factored into an enum: the enum is "Pending | Success[Result] | Failure[Error]".

Well, except the problem is a bit more nuanced because the pending-states also need to be consistent among the different subtasks: there is a dependency graph among them. So you should actually have an enum that says:

    Inventory_Pending
    Inventory_Failure[Error]
    Inventory_OK_Billing_Pending[InventoryData]
    Inventory_OK_Billing_Failure[InventoryData, Error]
    Inventory_OK_Billing_OK_Shipping_Pending[InventoryData, BillingData]
    Inventory_OK_Billing_OK_Shipping_Failure[InventoryData, BillingData, Error]
    Inventory_OK_Billing_OK_Shipping_OK[InventoryData, BillingData, ShippingData]
See, you would have had 3x3x3 = 27 valid states before for the Order but we have reduced to only the 7 legal states. Yay!

But now consider e.g. the following mutation. On Failure cases the executives at our company mandate that we never return a failed Order to a Pending status, rather we must always create a separate Order. This Order might skip inventory and/or billing and those need to be represented separately, as Inventory Skipped[OrderID] or InventoryAndBillingSkipped[OrderID]. So now our list of states following the "no unrepresentable state" logic, should really be:

    [... the 7 above, plus ...]
    Inventory_Skipped_Billing_Pending[OrderID]
    Inventory_Skipped_Billing_Failure[OrderID, Error]
    Inventory_Skipped_Billing_OK_Shipping_Pending[OrderID, BillingData]
    Inventory_Skipped_Billing_OK_Shipping_Failure[OrderID, BillingData, Error]
    Inventory_Skipped_Billing_OK_Shipping_OK[OrderID, BillingData, ShippingData]
    Inventory_And_Billing_Skipped_Shipping_Pending[OrderID]
    Inventory_And_Billing_Skipped_Shipping_Failure[OrderID, Error]
    Inventory_And_Billing_Skipped_Shipping_OK[OrderID, ShippingData]
Now someone else wants to add remediation actions, but only to remediate the exact error in the failure state, so _Failure is going to mean "no remediation taken" but we need to add some _Remediation with a boolean saying whether that process has completed or not. So we add:

    Inventory_Remediation[Error, Bool, Array[RemediationEvent]]
    Inventory_OK_Billing_Remediation[InventoryData, Error, Bool, Array[RemediationEvent]]
    Inventory_OK_Billing_OK_Shipping_Remediation[InventoryData, BillingData, Error, Bool, Array[RemediationEvent]]
    Inventory_Skipped_Billing_Remediation[OrderID, Error, Bool, Array[RemediationEvent]]
    Inventory_Skipped_Billing_OK_Shipping_Remediation[OrderID, BillingData, Error, Bool, Array[RemediationEvent]]
    Inventory_And_Billing_Skipped_Shipping_Remediation[OrderID, Error, Bool, Array[RemediationEvent]]
We're only up to 21 total states so far which is still probably manageable? But these changes do demonstrate exponential growth, which is a technical term that means that the growth of each step is some small fraction of the total growth that has happened up until that point. Because everything depends on Inventory (it's at the root of the tree), when we add a new state that the Inventory can be in (Skipped) we have to add enum cases for all of the other states, and we pay a cost proportional to the size of the tree. Similarly when everything can have an error (at the leaves of the tree), when we add a new uniform requirement for errors we have to add new leaves all throughout the tree and we pay a cost proportional to the size of the tree. (Another thing to notice about the Remediation state is that it is a Pending state for another Subtask that could have been added to the original Order whenever something moves into Failure mode.)

You get something powerful by reducing the 256ish-or-whatever states into the 21 legal states; you have a compile-time assurance that no bugs in your code have created weird states that can propagate their weirdnesses throughout the system. But you also have to maintain the 21 legal states all at once, instead of maintaining 4 subtasks each having one of 4 statuses.

Re: Functional Programming Self-Affirmations

#79

Earlier quoted context omitted.

Imagine you have a program where there are 4 options, W, X, Y, Z. Y and Z can't be set at the same time, and X can't be set unless W is set. If Y is set then X must be set as well. How do you represent this in a way that makes it impossible, even through programmer error elsewhere in the program, to have the flags in an invalid state? You can create en enum that looks like: enum program_state = ( W_X_Y_NZ, W_NX_NY_Z,…

And the Make Impossible States Unrepresentable crowd program like that?

It is not too bad in languages with discriminated unions. It's also not hard to fake discriminated unions in languages without them, even if you will miss some of the niceties.

Rather than thinking of it as an enum, think of it as a list of contructors:

    class ProgramState {
        bool w, x, y, z;

        ProgramState(x, z) // implies y = true, w = true
        ProgramState(w, z) // cannot set x; implies y = false (cannot set y)
    }
Even if the class needs all four fields, internally to represent all the possible combinations of data, there's no constructors/setters to work with them independently. (Which is also related to why "Make Impossible States Unrepresentable" also generally implies immutable, no setters at all makes it much easier to make states impossible.)

In languages with discriminated unions you might even have some natural field sharing depending on how your "constructors" are written and the memory expansion isn't necessarily "exponential".

Re: Functional Programming Self-Affirmations

#80

Earlier quoted context omitted.

Maybe I'm spoiled by TypeScript but this is how I'd do it in a structural typing system: type ConstrainedByW = | { w: false, x: false } | { w: true, x: bool } type ConstrainedByY = | { x: bool, y: false, z: bool } | { x: true, y: true, z: false } type ProgramState = ConstrainedByW & ConstrainedByY

Yes, but certainly you can see how even a toy example with just 4 booleans is already getting complicated?

It's certainly complicated. But not WTF-level complicated.

Complexity is both subjective and relative. Allowing invalid states in your model is going to complicate things, too.

I wouldn't mind having this example in production. It will scale just fine with the number of booleans.

Post reply on HN