Skip to content

Instantly share code, notes, and snippets.

@kbilsted
Last active November 13, 2022 00:38
Show Gist options
  • Save kbilsted/abdc017858cad68c3e7926b03646554e to your computer and use it in GitHub Desktop.
Save kbilsted/abdc017858cad68c3e7926b03646554e to your computer and use it in GitHub Desktop.
@maletor
Copy link

maletor commented Sep 19, 2020

You mean Haskell?

@olleharstedt
Copy link

You mean Haskell?

Haskell does in fact not enforce this design pattern. It's just pure discipline from the programmer's side to not put IO monad everywhere.

@maletor
Copy link

maletor commented Sep 23, 2020

You know what, that's a good point. At the end of the day there isn't too much "effectful" code:

  • Net/HTTP/Socket I/O
  • File I/O
  • Random I/O
  • Logging

Maybe a few others. It really seems like it wouldn't be too hard to isolate these at the top. The real challenge is keeping a handle on the imperative code at the top when your pure code depends on outputs during the intermediary steps, because it inevitably will.

@olleharstedt
Copy link

olleharstedt commented Sep 24, 2020

The real challenge is keeping a handle on the imperative code at the top when your pure code depends on outputs during the intermediary steps, because it inevitably will.

Yeah, this is the issue. :) Whenever you do IO, you might need to check if it was successful, for example, and then take different action depending on the result. That's why you can't just gather all IO in command objects and do them "later", or after the pure code is done. I had an idea about "stage-oriented programming", where each stage is a read-process-write pipeline, and you put any number of those after each other. The "process" stage will always be pure this way. I've outlined another alternative here, where generators/yield are used to bubble side-effects up, wrapped in promises (PHP): https://old.reddit.com/r/PHP/comments/ifnrvu/effect_types_in_php_using_psalm_and_amphp/

/**
 * @param string $path Remote file location
 * @param int $file Binary blob
 * @return Generator<WriteIO|HttpIO>
 * @throws Exception
 * (If you change the return type, Psalm won't compile.)
 */
public function saveFileToCloud(string $path, int $file): generator
{
    yield $this->writer->write('Saving file ' . $path);  // Yield WriteIO

    $request = new Request('https://google.com', 'POST');
    $request->setBody($file);

    $response = yield $this->http->request($request);  // Yield HttpIO (like curl)

    if ($response->getStatus() === 200) {
        yield $this->writer->write('Successfully saved file ' . $path);
    } else {
        $msg = 'Failed to save file ' . $path;
        yield $this->writer->write($msg);
        throw new Exception($msg);  // Exceptions are not part of the effect types (but could be in another type system)
    }
}

But as for the IO monad, this does not enforce functional core.

@maletor
Copy link

maletor commented Sep 24, 2020

@olleharstedt, check this out: effects-as-data.

It's linked from redux-saga. Whereas redux is typically thought of as client side concern, it can make sense in a backend (within request response loop). And redux-saga looks like a great pattern, using generators to pause execution and handle side effects separate from data and declare these interpreters.

I think we "figured it out" relatively quickly. Great chatting with you. There will, of course, always be more to learn.

@kbilsted
Copy link
Author

Please elaborate on exception handling in these frameworks and interpreter implementations.. it seems when you go down the interpreter route you have to implement a lot of details, say exceptions!

@olleharstedt
Copy link

olleharstedt commented Sep 24, 2020

@kbilsted Both exception handling and division by zero is not dealt with here, I think. You can check out experimental programming languages like Koka and F* that deal with this properly. Again, this does not enforce functional core, only more side-effect discipline. It Koka, they mark a difference between total and pure functions, the latter allowing both exceptions and division by zero.

@maletor
Copy link

maletor commented Sep 24, 2020

I encourage you to read about exception handling. There's nothing much to it.

https://redux-saga.js.org/docs/basics/ErrorHandling.html

Then you could put errors on the so called "root" as well.

Like I said, nothing much to it. The generator pattern is powerful.

@kbilsted
Copy link
Author

@maletor thanks, essentially you just removed the ability to throw an exception one place and catch in another by marking it an explicit codepath which you need to cater for all the way from your catch-statement down to your throw statement.

@kbilsted
Copy link
Author

should we convert part of this debate into text somehow? Should I convert this gist to a repo so we can grow this resource together?

@olleharstedt
Copy link

(You also get concurrency for free with generators - by yielding an array of promises: https://amphp.org/)

@olleharstedt
Copy link

(And another aspect with effects-as-promises is that you can mock the effects instead of the dependencies when designing your tests. The downside being that you bind the test more tightly to your implementation. But there are several upsides as well - e.g. testing only certain types of effects, being able to ignore all effects, and less mocking code.)

@olleharstedt
Copy link

should we convert part of this debate into text somehow? Should I convert this gist to a repo so we can grow this resource together?

Dunno. The pattern would have to be tried out in a bigger application, I think, before knowing if it's applicable.

@olleharstedt
Copy link

Oh, you already made a repo. Nice. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment