paldepind / functional-frontend-architecture

A functional frontend framework.
MIT License
1.44k stars 87 forks source link

ZipCodes could use Future instead of promises #13

Open alexeygolev opened 9 years ago

alexeygolev commented 9 years ago

I'm experimenting with elm-to-js as well (I guess you already know from twitter). I'm yet to switch to your union-types though:) One thing that could be nice, taken that you/we are going down a functional rabbit hole, is to use fantasy land data types. Future could be nice for the ZipCodes example in particular (and really close to what is happening in the elm code). Using Future we can do it like here

ericgj commented 9 years ago

I agree! I have started to play around with ramda-fantasy futures too. (I was the one who converted over the zipcodes example to snabbdom + flyd here). I was thinking of just wrapping XHR as described here, instead of an external library (superagent-future), but otherwise similar to your code.

The other hesitation I had was that Elm's Tasks seem more specialized than Futures to me and I wanted to understand them better. But if you want to open a PR, I'm sure @paldepind will give excellent feedback!

alexeygolev commented 9 years ago

@ericgj @paldepind the problem with just wrapping XHR is that when we need to POST things or PUT things we'll have to write wrappers for those as well. Which will effectively end up being a new xhr library. super-agent future is actually my hack of superagent... I just tap into its .end method. Elm's Tasks are quite similar to Futures.

Task Future Fantasy Land specification
1 succeed : a -> Task x a Future.of pointed Functor
2 fail : x -> Task x a Future.reject
3 map : (a -> b) -> Task x a -> Task x b Future.map Functor
4 andMap : Task x (a -> b) -> Task x a -> Task x b Future.ap (as far as I see it) Apply (which together with 1 makes it an Applicative)
5 andThen : Task x a -> (a -> Task x b) -> Task x b Future.chain Chain (together with 1 makes it a Monad)
6 sequence : List (Task x a) -> Task x (List a) Nope:-( Traversable
7 onError : Task x a -> (x -> Task y a) -> Task y a Future.chainReject
8 mapError : (x -> y) -> Task x a -> Task y a Future.bimap() with identity passed to resolve branch
9 toMaybe : Task x a -> Task y (Maybe a) Nope but could be done manually
10 fromMaybe : x -> Maybe a -> Task x a Nope, but could be done manually
11 toResult : Task x a -> Task y (Result x a) Nope, but...:-P
12 fromResult : Result x a -> Task x a Nope and I'm not sure about the use case

Then there are spawn and sleep that are quite esoteric for me (although it seems like spawn is something like fork...maybe) and a bunch of various arity maps that can be done manually. As Result is basically just a data type

data Result = Err String | Ok a

We can just return it from our .fork

At least that's how I see it.

ericgj commented 9 years ago

Thanks for putting together this table, very useful. They are similar... if you squint ,)

One difference is Elm Tasks don't have fork, as far as I understand the side effects are isolated to when ports are assigned tasks, which is where the chain would be triggered from. I could be wrong about that, I have only a superficial understanding of Elm code. But I found this recent writeup about Elm tasks, ports, mailboxes, addresses, etc. helpful. Probably not something we need to worry about since we are already in messy javascript land and don't need an explicit port.

Another thing that confused me was the 'free' error type in the Task, how that would translate to JS, but I think that's so you can get from/to the Result Err type in fromResult and toResult. I don't think it has any equivalent in dynamic JS land.

Re. spawn and sleep, looks like those are for executing tasks in a separate thread?

Anyway, it's up to @paldepind of course, but maybe you'd want to drop your code in as a separate example here -- zipcodes-with-futures or something? Might be nice to have both a native-promise-based one as well as a fantasy-future one, to compare.

alexeygolev commented 9 years ago

@ericgj I'm going to research into Elm threads and get back to you on spawn and sleep. I've been meaning to do it for quite a while. Elm hides things like .runIO, .fork, and other 'unsafe' monad operations from user. In the same way as Haskell 'kind of' does it with main function. We don't have this luxury so the only way is to actually run fork at some point. I'm going to see if I can actually push the .fork down even further

raine commented 9 years ago

Looking forward to seeing an implementation with Future.

:+1: on separate example.

paldepind commented 9 years ago

Hello all!

I've just pushed a zip codes example that uses futures. I've also made a few other improvements to the example. For instance while a result is being loaded it is indicated with a text.

I'd love to hear your opinions on the example!

ericgj commented 9 years ago

Nice! I learned a lot reading through this.

I like the way your top-level update works. [State, [Future]] seems like a useful abstraction of a typical async task -- e.g. setting some immediate state and then triggering one or more requests which resolve to other actions. And it puts all the state changes together at the 'DOM adapter layer' instead of within the app itself. (It also kind of solves the problem @yelouafi mentioned in snabbdom/#14, although the patch function is pulled out here.)

Nice also to see ramda lenses in action.

paldepind commented 9 years ago

Nice! I learned a lot reading through this.

Great. I'm happy to hear that.

[State, [Future]] seems like a useful abstraction of a typical async task -- e.g. setting some immediate state and then triggering one or more requests which resolve to other actions.

Thank you. I like it as well. Using an array makes it very easy to trigger multiple or zero actions. For instance if a component is initialized both the parent and a subcomponent might want to make requests.

It also kind of solves the problem @yelouafi mentioned in snabbdom/#14, although the patch function is pulled out here.

Yes. You're right. It does in fact. Calling the update function is handled in the top and one can only trigger async operations based on the current given state (which I think is sensible).

yelouafi commented 9 years ago

(It also kind of solves the problem @yelouafi mentioned in snabbdom/#14, although the patch function is pulled out here

Yes i was thinking of something similar to adapt from the Elm async solution. And it seems that the Effects concept of Elm maps quite naturally to Promises/Futures in JavaScript (Another possibility is to use reactive streams instead of Futures/Promises like flyd or Rx which makes it possible to trigger a stream of actions like a countdown a button).

The issue with deeply nested components can also be solved here: the future action bubbles up through parent components so they are able to wrap the 'Future Action' in another Future Action like they wrap 'normal actions' triggered from the view of child compnents (elm example of Pair of random GIF viewers).

ericgj commented 9 years ago

The issue with deeply nested components can also be solved here: the future action bubbles up through parent components so they are able to wrap the 'Future Action' in another Future Action like they wrap 'normal actions' triggered from the view of child compnents (elm example of Pair of random GIF viewers).

Yeah. I wish I understood this better, it seems crucial.

In Elm, Effects.map (which does this 'wrapping') looks like this

map : (a -> b) -> Effects a -> Effects b
map func effect =
  case effect of
    Batch effectList ->
        Batch (List.map (map func) effectList)

-- skipping Task, None and Tick effect types

Is that effectively the same as

// in a parent component update function
const [childState, childFutures] = childComponent.update(someAction, childState);
return [ R.set(childLens, childState), R.map( (future) => future.map(parentAction), childFutures ) ];

?

So essentially when the future resolves from the top, the result will be chained through actions from the originating component at the bottom, bubbling up to the top?

I have to see it to believe it...!

ericgj commented 9 years ago

FYI, I tried out this future chaining to handle nested updates from HTTP requests (PR #16) -- it works great!