Remix.run Logo
agentultra 2 days ago

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.

greener_grass 2 days ago | parent | next [-]

I'm not convinced that you can follow all of these outside of Functional Programming.

How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?

How can you do "Errors as values" at a large scale without do-notation / monads?

How can you do "Functional core, imperative shell" without the ability to create mini DSLs and interpreters in your language?

bunderbunder 2 days ago | parent | next [-]

Maybe not in literally every language, but, to cherry pick some examples:

Java (along with many other object-oriented languages) lets you create objects that are effectively immutable by declaring all fields private and not providing any property setters or other methods that would mutate the state.

Errors as values is one of the headline features of both Go and Rust, neither of which has do notation and monads.

Functional core, imperative shell is something I first learned in C#, but I don't think it was _really_ at significantly more of a disadvantage than most other languages. The only ones that let you really enforce "functional core, imperative shell" with strong language support are the pure functional languages. But people working in, say, Ocaml or Scala somehow still survive. And they do it using the same technique that people working in Java would: code review and discipline.

None of this is to say that language-level support to make it easier to stick to these principles is not valuable. But treating it as if it were a lost cause when you don't have those features in the language is the epitome of making the perfect the enemy of the good.

neonsunset 2 days ago | parent | next [-]

Coincidentally, "functional core, imperative shell" is how most companies get to adopt F# from what I was told (and had seen). It integrates really well. C# itself is a proper multi-paradigm language nowadays, and so is also quite successful at employing functional constructs.

timClicks 2 days ago | parent | prev [-]

I consider Rust's Result<T, E> and Option<T> to be monads. Is this incorrect?

bunderbunder 2 days ago | parent [-]

Depending on what functions are in there, they are. But you can make types that happen to be monads in C, too. All you need is a datatype with `return` and `bind` functions that follow a certain spec.

What makes Haskell different is that it has a language-level concept of a monad that's supported by special syntax for manipulating them. (C# does, too, for what it's worth.) Without something like that, observing that a certain type can be used as a monad is maybe more of a fun fact than anything else.

(ETA: for example, many, many languages have list types that happen to be monads. But this knowledge probably won't change anything about how you use them.)

dllthomas 2 days ago | parent | prev | next [-]

> How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?

I think you're confusing "make illegal states unrepresentable" with "parse, don't verify"? If your type cannot represent any invalid states, there's no way you can reach them through mutation.

matt_kantor 2 days ago | parent [-]

The "sequences of mutations" phrasing made me think they were talking about stuff like state machines or handles for external resources—for example calling `databaseConnection.close()` on an already-closed connection, which is usually a runtime error (or maybe a no-op).

bunderbunder a day ago | parent [-]

I don't see the problem with state machines. When you're dealing with something that handles external input, "the input is invalid" is, for your program, a valid state. For example a regular expression engine doesn't try to make it impossible to pass in a string that doesn't match; it just returns a result indicating that the string didn't match.

Database connections are exactly what the "functional core, imperative shell" principle is about. The idea is to handle all I/O at the boundary. So shell is never passing open database connections into the functional core; it's instead retrieving everything that's needed up front so that the core can be deterministic.

"Functional core, imperative shell" might actually be my favorite of the principles, because it makes code so much easier to test. Every time I come across a codebase whose test suite has to make intense use of mocking to cope with how they allowed concurrency to spread throughout every single layer and module in the application, I get a little bit sad that nobody did its authors the service of teaching them that you don't actually need to make your own life hard like that.

It also tends to result in less code to understand and maintain overall, IME. Because if you limit the number of places where an error is even possible, you don't get stuck having to litter your codebase with excess (and often repetitive) error handling code.

matt_kantor a day ago | parent [-]

I agree with all of that, but I don't see what it has to do with this thread of the discussion.

The comment I replied to was wondering what exactly greener_grass was referring to when they said:

> outside of Functional Programming […] How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?

And my guess was that they had illegal transitions between states in mind. Those are hard/impossible to statically reason about when the program is written as "sequences of mutations" (particularly when aliasing is possible).

do_not_redeem 2 days ago | parent | prev | next [-]

> How can you "Make illegal states unrepresentable" with mutable state

By either making the data const, or encapsulating mutable state with private fields and public methods

> How can you do "Errors as values"

Go, Rust, Odin, Zig, and many more are imperative languages that do exactly this

> How can you do "Functional core, imperative shell"

Write a function that takes the old state and returns the new state

dllthomas 2 days ago | parent [-]

> By either marking your data as const, or encapsulating mutations with private fields and public methods

That would seem to be making illegal states unreachable rather than unrepresentable, closer in spirit to "parse, don't verify".

do_not_redeem 2 days ago | parent [-]

GP seemed more worried about maintaining invariants in the face of mutability, so that's what my answer spoke to.

For modeling the data in the first place, just use the right combination of sum types, product types, and newtypes - that's not specific to functional languages. I'm sure GP knew this already without me saying it. Sum types may have been a "functional programming" thing a few decades ago but they aren't anymore.

enugu 2 days ago | parent | prev | next [-]

"Make illegal states unrepresentable" can be done by encapsulating the variables inside a single data object(struct/class/module) and only exporting constraint respecting functions. Also, Algebraic Data Types can be present in FP/non-FP languages.

The Result monad can be implemented in any static language with generics (just have to write two functions) and in a dynamic language this is easy (but return will have to be like T.return as there is no implict inference).

I didn't get the relation between FCore/IShell and DSLs, the main requirement for FCore is a good immutable library. Macros help DSLs though that is orthogonal.

But really, my main point is that OOP vs FP is red herring as 3/4 aspects which characterize OOP can be potentially done in both OOP and FP, with different syntax. We shouldn't conflate the first 3 with the 4th aspect - mutability.

An OOP language with better extension mechanism for classes +immutable data structure libraries and a FP language with first class modules would converge. (ref: Racket page below and comment on Reason/OCaml down the page).

See Racket page on inter-implementability of lambda, class, on the unit(ie. a first-class module) page here (https://docs.racket-lang.org/guide/unit_versus_module.html). Racket has first class 'class' expressions. So, a mixin is a regular function.

solomonb 2 days ago | parent | prev | next [-]

> 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.

greener_grass 2 days ago | parent [-]

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.

solomonb 2 days ago | parent | next [-]

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.
kccqzy 2 days ago | parent | prev | next [-]

That's merely syntax sugar. Haskell doesn't even have this sugar and so in a monad you have to bind explicitly, and it's fine. It's not a pyramid of doom.

greener_grass 2 days ago | parent [-]

Haskell has do-notation which is a more general kind of this syntactic sugar

2 days ago | parent | next [-]
[deleted]
kccqzy 2 days ago | parent | prev [-]

Yeah that's what I said about bind.

Let's consider a language like C++ where it has none of those syntax sugars. The absl::StatusOr is a class with the idea that errors should be values. The standard library has std::expected since C++23. So where is your pyramid of doom?

dllthomas 2 days ago | parent | prev [-]

`?` and `let ... else` both partially address this, yeah.

tubthumper8 2 days ago | parent | prev | next [-]

Genuinely curious for all these follow up questions:

Is immutability exclusive to functional programming?

Is the ability to use data/values exclusive to functional programming?

Are monads exclusive to functional programming?

For discussions like this, how do we separate "it was done first in functional programming but can also be done in procedural programming" with "it cannot be followed outside of functional programming"?

greener_grass 2 days ago | parent [-]

> Is immutability exclusive to functional programming?

No, but immutable defaults are powerful.

E.g. in JavaScript / Python, the built-in lists and dictionarys (which are blessed with special syntax) are mutable.

> Is the ability to use data/values exclusive to functional programming?

No, but expression-orientation makes this less painful

> Are monads exclusive to functional programming?

You can hack them in by abusing co-routines or perhaps async/await in various languages, but it will never be as good as something built for this purpose.

Type-inferences, type-classes and do-notation make monads workable in practice.

yazzku 2 days ago | parent [-]

You don't need coroutines or async or anything complicated to model monads, just functions and data structures. Search for "c++ monads" and you'll find a ton of examples.

greener_grass 2 days ago | parent [-]

You need the syntax if you want it to actually work well in practice.

louthy 2 days ago | parent | prev [-]

As someone who's written a pure functional framework for C# [1], I'll bite...

> How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?

Firstly, don't use mutable state, write immutable types. Secondly, write constructors that reject poorly formed data structures. Thirdly, for existing libraries with types that are mutable, create a wrapper for the library with functions that return an IO/effect monad.

> How can you do "Errors as values" at a large scale without do-notation / monads?

Luckily, from a C# PoV, we have LINQ, which is equivalent to do-notation. I agree that manual management of monadic flow would be hard without something akin to do-notation or LINQ.

You can get quite far with fluent methods, but a general monadic-bind is quite hard to chain if you want to carry all of the extracted values through to subsequent expressions (lots of nesting), so yeah, it would not be ideal in those languages. It should be stated that plenty of functional languages also don't have do-notation equivalents though.

> How can you do "Functional core, imperative shell" without the ability to create mini DSLs and interpreters in your language?

I've never really liked the "Functional core, imperative shell" thing. I think it's an admission that you're going to give up trying to be functional when it gets difficult (i.e. interacting with the real world). It is entirely possible to be functional all the way through a code-base.

In terms of DSLs: I'm not sure I know any language that can't implement a DSL and interpreter. Most people don't realise that the Gang of Four Interpreter pattern is isomorphic to free-monads, so most imperative languages have the ability to do the equivalent of free-monads.

As the GP states, it takes discipline to stick to the constraints that a language like Haskell imposes by default. Not sure about the charisma part!

I have found that having access to a world-class compiler, tooling, and large ecosystem to be more valuable to getting shit done than the exact language you choose. So, bringing the benefits of the pure-FP world into the place where I can get shit done is better than switching to, say Haskell, where it's harder to get shit done due to ecosystem limitations.

There's also the get out of jail free card, which allows me to do some gnarly high-performance code in an imperative way. And, as long as I wrap it up in a function that acts in a referentially transparent way, then I can still compose it with the rest of my pure code without concern. I just need to be a bit more careful when I do that and make sure it's for the right reasons (i.e. later stage optimisations). That's less easy to do in FP languages.

Again, it's about discipline.

If you want to see how this can look in a mainstream, imperative-first, language. I have a few samples in the repo, the one I like to share when helping OO-peeps learn about monads and monad-transformers is this game of 21/Pontoon [2]. I suspect most people won't have seen C# look like this!

[1] https://github.com/louthy/language-ext/

[2] https://github.com/louthy/language-ext/blob/main/Samples/Car...

agentultra 2 days ago | parent [-]

> Not sure about the charisma part!

Software developed on a team where everyone has different values/principles... some times it's not the technical discipline that is required, it's convincing folks to adopt these patterns and stick with them that's the hard lift. :)

jefffoster 2 days ago | parent | prev [-]

Mostly functional programming does not work (https://queue.acm.org/detail.cfm?id=2611829)

louthy 2 days ago | parent [-]

I have a lot of respect for Erik Meijer and I agree with the basic premise of the paper/article. However, I don't fully agree with Erik's position.

Let's say this was my program:

    void Main()
    {
       PureFunction().Run();
       ImpureFunction();
    }
If those functions represent (by some odd coincidence) half of your code-base each (half pure, half impure). Then you still benefit from the pure functional programming half.

You can always start small and build up something that becomes progressively more stable: no code base is too imperative to benefit from some pure code. Every block of pure code, even if surrounded by impure code, is one block you don't have to worry so much about. Is it fundamentalist programming? Of course not. But slowly building out from there pays you back each time you expand the scope of the pure code.

You won't have solved all of the worlds ills, but you've made part of the world's ills better. Any pure function in an impure code-base is, by-definition: more robust, easier to compose, cacheable, parallelisable, etc. these are real benefits, doesn't matter how small you start.

So, the more fundamentalist position of "once one part of your code is impure, it all is" doesn't say anything useful. And I'm always surprised when Erik pulls that argument out, because he's usually extremely pragmatic.