purescript-flare
purescript-flare copied to clipboard
Can we use free applicative functors for Flare?
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:
- An ADT for
Element
which simply remembers the components which we have to set up (something likedata Element = InputInt Label Int | InputBool Label Bool | ..
which stores the label and default values). This is the easy part [*]. - A structure which remembers the transformations to the actual data (
map
s andapply
s).
@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.
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.
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 inlift2 pow <$> ..
). I believe this could be solved by adding yet another layer on top of the free applicativeFreeAp 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
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 b
s and e
parameters align. Might not be the greatest, but I can write it up if you are interested.
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!
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!
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 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 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 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.
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.
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?
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.
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.
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.
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
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.
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.
For reference, purescript-freeap version 0.1.0 is available on bower.
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.
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).
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.
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.