November 26th, 2024

Functional Programming Self-Affirmations

Dmitrii Kovanikov emphasizes the importance of functional programming principles in Swift, advocating for practices that enhance code quality, reliability, and maintainability through better data handling and error management.

Read original articleLink Icon
DisagreementSkepticismAppreciation
Functional Programming Self-Affirmations

Dmitrii Kovanikov discusses the relevance of functional programming principles in mainstream programming languages, particularly Swift. He highlights five self-affirmations related to functional programming: "Parse, don’t validate," "Make illegal states unrepresentable," "Errors as values," "Functional core, imperative shell," and "Smart constructor." Each principle emphasizes improving code quality and maintainability. For instance, "Parse, don’t validate" suggests enriching data through parsing rather than merely validating it, while "Make illegal states unrepresentable" advocates for designing logic and types to prevent bad states. The concept of "Errors as values" promotes returning error values instead of raising exceptions, enhancing predictability. The "Functional core, imperative shell" principle encourages isolating pure functions from side effects, making testing easier. Lastly, "Smart constructor" focuses on ensuring valid data construction, which can optimize performance and clarity. Kovanikov concludes that while these ideas are rooted in functional programming, they can be beneficially applied in imperative programming as well, leading to simpler and more robust code.

- Functional programming principles can enhance code quality in mainstream languages.

- Parsing enriches data, while validation merely checks correctness.

- Designing to prevent illegal states can reduce bugs and improve code reliability.

- Returning error values instead of exceptions leads to more predictable error handling.

- Isolating pure functions from side effects simplifies testing and improves clarity.

AI: What people are saying
The comments reflect a diverse range of opinions on the application of functional programming principles in Swift and their implications for code quality and error handling.
  • There is skepticism about the practicality of making illegal states unrepresentable, especially in dynamic domains with evolving data structures.
  • Many commenters discuss the merits and drawbacks of treating errors as values versus using exceptions, highlighting the importance of context in error handling.
  • Some agree that functional programming principles can enhance code quality, even in non-functional languages, but emphasize the need for discipline to implement them effectively.
  • Several commenters express that while the ideas presented are valuable, they are not new and are already well-documented in existing literature.
  • There is a consensus that the principles discussed can lead to better code organization and reliability, but practical implementation can be challenging.
Link Icon 17 comments
By @wesselbindt - 6 months
I do not work in a functional language, but these ideas have helped me a lot anyway. The only idea here that I find less directly applicable outside purely functional languages is the "Errors as values [instead of exceptions]" one.

On the surface, it makes complete sense, the non-locality of exceptions make them hard to reason about for the same reasons that GOTO is hard to reason about, and representing failure modes by values completely eliminates this non-locality. And in purely functional languages, that's the end of the story. But in imperative languages, we can do something like this:

  def my_effectful_function():
    if the_thing_is_bad:
      # do the failure thing
      raise Exception
      # or
      return Failure()
    return Success()
and a client of this function might do something like this:

  def client_function():
    ...
    my_effectful_function()
    ...
and completely ignore the failure case. Now, ignoring the failure is possible with both the exception and the failure value, but in the case of the failure value, it's much more likely to go unnoticed. The exception version is much more in line with the "let it crash" philosophy of Erlang and Elixir, and I'm not sure if the benefits of locality outweigh those of the "let it crash" philosophy.

Have any imperative folks here successfully used the "errors as values" idea?

By @agentultra - 6 months
These are great ideas and patterns even if you’re not doing functional programming.

FP-first/only languages tend to push you in these directions because it makes programming with them easier.

In languages where FP is optional, it takes discipline and sometimes charisma to follow these affirmations/patterns/principles.. but they’re worth it IMO.

By @beders - 6 months
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 enough thought was put into the definition of an error.

"Disk is Full" vs. "data input violates some business rule" are two very - very - different things and should be treated differently. Exceptions are the right abstraction in the first case. It's not in the second case.

"Functional core, imperative shell" - 100% agreement here.

By @aiono - 6 months
For "Errors as values", I agree 100% that it's better then special values or untracked exceptions but I also think that current programming languages lack the features that allow encoding errors as values conveniently. Firstly, there is no composition of errors. If I use a library for a network call and then use another library for a database query, now the possible errors should be the union of the errors that can be returned from the either of the functions. But most practical languages lack the mechanism to do that (except OCaml). One has to define a wrapper type just to encode that particular composition. And it won't work if I want to handle for example Not Found case but not Internal Server Error. I see this is because most statically typed languages have nominal typing and not structural typing. But it is a necessity for pretty much any software otherwise people will just see that tracking errors is too much trouble in terms of composition.
By @LudwigNagasena - 6 months
> 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 that a function may throw. How is it any less predictable than return values?

Semantically, a thrown exception is simply a return value with debug information that gets automatically returned by the caller unless specified otherwise. It is simply a very handy way to reduce boilerplate. Isn't it objectively better to not write the same thing over and over again?

By @beastman82 - 6 months
Completely agree with these.

One way to achieve "Make illegal states unrepresentable" is by using "refined" types, a.k.a. highly constrained types. There is a "refined" library in both Haskell and Scala and the "iron" library for Scala 3.

By @falcor84 - 6 months
I'm very disappointed. I was really hoping for something like the SRE affirmations - https://youtu.be/ia8Q51ouA_s
By @jandrese - 6 months
> 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 great idea until you have 20 options to account for and your enum is now 2^16 entries long. Then your company opens a branch in a different country with a different regulatory framework and the options list grows to 50 and you code no longer fits on a hard drive.

By @csours - 6 months
My ideal service layer has a functional core - easy to understand, easy to test.

linked from the article:

https://www.javiercasas.com/articles/functional-programming-...

By @enugu - 6 months
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 easy for both FP and non-FP systems).

Take an example like GUI components. Ideally, you should be able to compose several components into a single component (culminating in the app) and not have a custom implementation of a giant state store which is kept in something like Redux and you define the views and modifiers using this store.

Say, you have a bunch of UI components each given as a view computed as a function from a value and possible UI events which can either modify the value, remain unhandled or configurable as either. Ex: dialog box which handles text events but leaves the 'OK' submission to the container.

There are atleast two different kinds of composability (cue quote in SICP Ch1 by Locke) - aggregation and abstraction. Ex: Having a sequence of text inputs in the document(aggregation) and then abstracting to a list of distances between cities. This abstraction puts constraints on values of the parts, both individually(positive number) and across parts(triangle inequality). There is also extension/enrichment, the dual of abstraction.

This larger abstracted component itself is now a view dependent on a value and more abstract events. But, composing recursively leads to state being held in multiple layers and computations repeated across layers. This is somewhat ameliorated by sharing of immutable parts and react like reconciliation. But, you have to express your top->down functions incrementally which is not trivial.

By @revskill - 6 months
A class is just a function in the category of Javascript.
By @lupire - 6 months
This is fine but it's just a rehash of old well-knowned stuff.

I don't see the value of learning this stuff one random blog post at a time.

There are many books and established blogs with long series of material to give you an education.

By @munificent - 6 months
It's hard not to giggle when the conclusion right after "Smart constructors" says "Do these ideas belong only in functional programming? While they are practiced more there...".

Ah yes, because using constructors to ensure that new objects are in a valid state is virtually unheard of in object-oriented programming.