joaomilho / zealot

λ Server as a function for Node.js
10 stars 0 forks source link

Starting the discussion #1

Open xaviervia opened 7 years ago

xaviervia commented 7 years ago

I guess we can discuss here as good as via chat or better. I started playing around with building an example, and what struck me first was that it's a little unclear why it's necessary the result.chain in every middleware. I do understand why, but it took me reading through all the code to do it, so I guess it is something that could be documented.

Also, I understand why we flatMap and also chain: one is for the Stream, the other for the Result inside the stream. But it makes me queasy. In Task, for example, the Task can be rejected at any time and a rejected Task is pretty much like a Result.Error, there are even natural transformations for them… would it be possible to just use Streams which support Ok and Error constructors as well? That way, middlewares will just chain/flatMap only once. Same for mapping.

Just for illustration purposes, here is how my example looks due to the double map needed to get out of the Stream and Result:

on(
  ok.send('Hello'),
  map(map(server => (console.log(Object.keys(server)), server)), listen(3000))
)

I understand Folktale does not provide any such monad for continuous values, less so for values that can have individual failing status. I don’t have any suggestions about what to do, but flattening it into a single structure that you can just map and chain upon would be really nice to make it easy to understand.

joaomilho commented 7 years ago

So...

1) Using chain/flatMap is not required for users of middleware. This is just the initial example I've pushed, in the next iteration the json middleware will be just json(), since it makes sense than middleware doesn't leave this job to the user, since using map would result in a wrong usage. Of course, the creator of a middleware needs to understand what to do;

2) "I understand Folktale does not provide any such monad for continuous values". It will, but flyd really works fine, and it's dead simple.

3) Yes, it's unfortunate that the libs use different names: chain & flatMap, maybe we could reexport new ones for convenience.

4) I'm not sure we can avoid the double call in any way. Note map/map, chain/map, map/chain, chain/chain have completely different meanings:

Note there's also mapError, cata and other methods that can be used in the Result and also filter, throttle etc, that can be used on the Stream. To create an API with all combinations seem overkill.

joaomilho commented 7 years ago

I'll publish some new stuff soon and I guess this will be clearer.

xaviervia commented 7 years ago

I guess the point with trying to avoid the double map is that there is no really a point in mapping over a failing value, right? In a rejected Task, the rejection gets "fast tracked" avoiding all further calls to .chain and .map. I imagined that the same could apply here: having some continuous value construct that includes the Ok/Error status as part of it, so essentially Stream.Error would be a constructor of Stream, and if you want to indicate failure, you do so by chaining and rejecting in the chained Stream.

xaviervia commented 7 years ago

Trying to come up with an example of what I’m suggesting I end up realizing I just want to chain tasks, so the implied question is: why using a stream instead of a task?

joaomilho commented 7 years ago

1) Mmm, maybe I don't get it, but definitively are needs to map over failing values, both to recover and to emit errors to the client.

2) Check the repo now, there's even some docs published at joaomilho.github.io/zealot, very basic and incomplete.

3) On the Stream vs Task, think about this issue:

const delay = (value, seconds) => 
  Promise.new((resolve) => 
    setTimeout(resolve(value), seconds * 1000))

delay('OK', 5000).then(console.log)
// 5s after... 'OK'

This is a classic use case for a Promise/Task. Now rewrite the same using setInterval, where you can get a console log of 'OK' every 5 seconds.