purescript-flare icon indicating copy to clipboard operation
purescript-flare copied to clipboard

Can we use free applicative functors for Flare?

Open sharkdp opened this issue 8 years ago • 22 comments

Right now, we use the 'effectful' type

newtype UI e a = UI (Eff (dom :: DOM, chan :: Chan | e) (Flare a))

as our main applicative functor to create the user interfaces. Here, Flare is defined as

data Flare a = Flare (Array Element) (Signal a)

It seems unnecessary that we have to include the effects at this stage already. It would be much nicer to work with a 'pure' data type similar to Flare and only introduce the effects when actually running the UI (Flare itself is also an applicative functor).

My initial thinking was that it is necessary to include the effects, because we have to construct the HTML Elements (DOM) and set up the channels for the corresponding signals (Chan). It was very easy to do these things inside the Eff monad.

But if we are able to defer the actual setup, we could get rid of the effects. This would involve two things:

  1. An ADT for Element which simply remembers the components which we have to set up (something like data Element = InputInt Label Int | InputBool Label Bool | .. which stores the label and default values). This is the easy part [*].
  2. A structure which remembers the transformations to the actual data (maps and applys).

@paf31 @ethul: I'm not experienced enough to see this right away, but if one of you has some free time to have a short look, this would be great. My idea was that free applicative functors from purescript-freeap could actually be a good fit for point 2. As far as I understand, the free applicative functor 'remembers' all the transformations and I could run it via foldFreeAp at the point when I actually want to set up the UI (via a natural transformation from Flare to Signal??).

[*] Actually, this would make things like #2 much simpler to implement.

sharkdp avatar Nov 24 '15 15:11 sharkdp

Thanks for looking into purescript-freeap! I was trying out some ideas to see if it can be applied to Flare. Not totally sure yet if this is a feasible way to go, but I have a gist up with what I had in mind.

https://gist.github.com/ethul/c4ec55d18d5bc9138520

This definitely requires more work and is really just a start, but it might be worth some discussion.

ethul avatar Nov 26 '15 03:11 ethul

Wow, thanks for this! This is amazing.. and actually works (unlike my attempts :smile:). I was playing around with freeap yesterday and 'almost' got a working version, but always had problems to set up the event handlers because I lost the type information about the input fields. As far as I understand, this is exactly what you achieve by Flare Number -> a in

NumberUI Label Number (Flare Number -> a)

(which reminds me of the Reader functor).

If you wouldn't mind, I would try to clean this up a little bit and build a full version out of it.

A few things which I need to figure out:

  • It's a little bit unfortunate that UI now requires two levels of lifting (as in lift2 pow <$> ..). I believe this could be solved by adding yet another layer on top of the free applicative FreeAp UIF = UI with Functor and Apply instances which pass the function down to the deepest level.
  • I hope that we can still support lifting of Signals to UI's:
lift :: forall e a. Eff (chan :: Chan, dom :: DOM | e) (Signal a) -> UI e a

sharkdp avatar Nov 26 '15 11:11 sharkdp

Great! Glad this might work out for your use-case. Please feel free to use it as you like.

I agree that requiring lift2 is not ideal. I will have to think more on how to clean up that. However, adding another layer may do the trick.

For lift, would the following definition work instead?

lift :: forall a. Signal a -> UI (Flare a)
lift sig = pure (Flare [] sig)

Alternatively, we could add a constructor to UIF. Something like

data UIF a = ... | LiftUI (forall e b. Eff e (Signal b)) (forall b. Flare b -> a)

And then a smart constructor could ensure that the bs and e parameters align. Might not be the greatest, but I can write it up if you are interested.

ethul avatar Nov 26 '15 13:11 ethul

Great! Glad this might work out for your use-case. Please feel free to use it as you like.

Thanks! By the way, I'd love to see a release of freeap on bower and pursuit, if you were not planning to do that anyway.

I agree that requiring lift2 is not ideal. I will have to think more on how to clean up that. However, adding another layer may do the trick.

ok, I will try.

For lift, would the following definition work instead?

lift :: forall a. Signal a -> UI (Flare a)

This function is also useful (I called it wrap), but I'm afraid we also need lift, which is needed in example 7 to lift animationFrame to an UI component.

Alternatively, we could add a constructor to UIF. Something like [...] And then a smart constructor could ensure that the bs and e parameters align. Might not be the greatest, but I can write it up if you are interested.

This sounds good. I would also like to try it on my own, but I appreciate any (further) help, of course!

sharkdp avatar Nov 26 '15 17:11 sharkdp

I've added liftUI to the gist if you want to compare notes. Also, lift is in the gist to, which you call wrap. For liftUI it requires unsafeCoerce. I am not sure if there is a better way, but this seems to work.

Also, I'd be happy to make a release of purescript-freeap.

@paf31 would you be interested having purescript-freeap in purescript or purescript-contrib? Thanks!

ethul avatar Nov 26 '15 18:11 ethul

We could move the repo if you like. The reason to move to contrib is usually that you don't have time to maintain it, but want someone to maintain it, and we can move it to core if you want it to be supported as part of the regular psc releases (there, it doesn't really matter who does the maintenance, but it will get done).

paf31 avatar Nov 26 '15 19:11 paf31

@paf31 Understood. I am happy to maintain the repo. I was thinking core might be a viable option to place the library next to purescript-free. But I don't really have a preference. Do you have a preference one way or the other? However, if we do move it to core, do you have any comments on the naming of the exposed API? I am not attached to any of the data type or function names. Any feedback would be great!

ethul avatar Nov 26 '15 22:11 ethul

@ethul I finally managed to get my own attempt running:

https://gist.github.com/sharkdp/fd05d80badd9c87926e7#file-flare-freeap-purs-L67

It's quite a bit different from yours but I'm not sure which one is to favor. I'm not quite happy with the way the event handlers are treated in my approach...

Edit: The free-applicative branch includes a cleaned-up, working version with this approach. The only thing that is really missing is

foldp :: forall a b. (a -> b -> b) -> b -> Flare a -> Flare b

Concerning liftUI... I just realized that it is not that important, since we can just do

time <- animationFrame
runFlareDrawing "controls7" "output7" $
  animate <$> lift time <*> boolean "Shadow" false

(i.e. lift is sufficient, as you said).

sharkdp avatar Nov 27 '15 17:11 sharkdp

@sharkdp Looks great! I've added wrap and lift (as you originally named them) in a forked gist:

https://gist.github.com/ethul/7a33b3cca1d0f1db48bb

This seems like it should work.

ethul avatar Nov 28 '15 04:11 ethul

Thanks! I didn't know unsafeInterleaveEff. I have now cleaned up the free-applicative branch. The only thing I still didn't figure out is foldp.

Also, select :: forall a. (Show a) => Label -> a -> Array a -> Flare a doesn't work anymore (only if I restrict it to select :: Label -> String -> Array String -> Flare String).

In the end, I should probably evaluate if it's really worth making this step. Losing foldp would be a big disadvantage... on the other hand, I like the absence of effects in the Flare data type.

Thanks again for your help.

sharkdp avatar Nov 28 '15 13:11 sharkdp

Looks good. I attempted implementing foldp: https://gist.github.com/ethul/7a33b3cca1d0f1db48bb

Not sure if this is the best solution though. What do you think?

ethul avatar Nov 28 '15 15:11 ethul

I also added select. However, I just mapped the Array a to Array String early using the required Show instance. Not sure if this is what you want though.

ethul avatar Nov 28 '15 15:11 ethul

Not sure if this is the best solution though. What do you think?

Well, it works! Thanks! It's a little bit unfortunate that unsafeCoerce is needed. Also, (being nit-picky) the types in the Foldp constructor are wrong in the sense that there shouldn't be a forall b, but rather a specific b.

I also added select. However, I just mapped the Array a to Array String early using the required Show instance. Not sure if this is what you want though.

Yes.. this is what I meant by "restricting to select :: ... -> Flare String". The problem is, that your select can not return a Field a (or Flare a) because only the strings are stored in the Component. But this might be an acceptable drawback.

sharkdp avatar Nov 28 '15 16:11 sharkdp

Welcome! For Foldp, it is unfortunate about the unsafeCoerce, but if those details are kept internal (by not exporting any of the Cell constructors, maybe it's not so bad. Gotcha about select. I think we can do this another way, similar to how Foldp works. I can write up something up. Not completely sure it would work though.

ethul avatar Nov 28 '15 17:11 ethul

I've updated select. Not pretty, but again if the constructors of Component are not exported, then maybe it would work out okay.

https://gist.github.com/ethul/7a33b3cca1d0f1db48bb

ethul avatar Nov 28 '15 17:11 ethul

Thanks. I would like to think a little bit about this .. but you are probably right. If everything is hidden from the user, I suppose nothing bad can happen.

sharkdp avatar Nov 28 '15 20:11 sharkdp

Understood. My thought is that if the constructors are not part of the external API then basically the unsafe coercion becomes internal details. And I believe they are kept safe from the exported smart constructor functions.

ethul avatar Nov 28 '15 21:11 ethul

For reference, purescript-freeap version 0.1.0 is available on bower.

ethul avatar Dec 10 '15 23:12 ethul

Thanks! I'm still thinking about this :smile:. I found a slightly 'cleaner' way to support foldp, by just writing

foldp :: forall a b. (a -> b -> b) -> b -> Flare a -> Flare b
foldp f s0 = map (foldp_ f s0)

which does not need unsafeCoerce and the FoldP (forall ..) .. type but instead uses a foreign function foldp_ to hold the state/accumulator:

exports.foldp_ = function(f) {
  return function(seed) {
    var acc = seed;
    return function(x) {
      acc = f(x)(acc);
      return acc;
    };
  };
};

In this sense it is still 'unsafe' but that's actually the same thing that Signal.foldp does.

sharkdp avatar Dec 11 '15 20:12 sharkdp

I just noticed that this implementation of foldp actually has some serious issues. It calls the function f even if the input value has not changed (but some other part of the Signal).

sharkdp avatar Dec 11 '15 21:12 sharkdp

Understood on the freeap decision. And maybe there's a way to write foldp in the direction your going. Not sure off the top of my head though.

ethul avatar Dec 11 '15 22:12 ethul

I have created a free applicative DSL using freeap - https://github.com/rintcius/purescript-aui . It targets Flare and also has a dat.GUI interpreter. All still early stage, but the dat.GUI example contains a foldp (not sure if it has the same issues that @sharkdp mentions). Let me know if you have any comments.

rintcius avatar Nov 11 '16 21:11 rintcius