miso icon indicating copy to clipboard operation
miso copied to clipboard

Zooming in the Effect(Core) monad / exporting EffectCore

Open noinia opened this issue 9 months ago • 8 comments

Is your feature request related to a problem? Please describe.

I'm trying to update some of my code to the new miso 1.9 setup. However, I'm getting stuck / I don't think miso currently exports everything I need in order to update. Before, my code looked something like this (this example is somewhat simplified):

type Field = ... 
data Model = Model { _field :: Field } 
makeLenses ''Model
data MyAction = ... 

fieldUpdate :: Field -> MyAction -> Effect action Field

update           :: Model -> MyAction -> Effect action Model 
update m act = m&field %%~ flip fieldUpdate act 

I think the solution would now need to look something along the lines of:

fieldUpdate :: MyAction -> Effect Field action 
fieldUpdate = .. 

update       :: MyAction -> Effect Model action 
update act = zoom field $ fieldUpdate action

but for that EffectCore needs to be an instance of Zoom (from the lens package). However, I cannot implement that instance as miso is not exporting EffectCore. None of the other lensy type combinators seemed to apply for this case, i.e %= or .= take a pure function or value rather than a monadic one.

Describe the solution you'd like

  1. At least export EffectCore so that I can implement the appropriate Zoom instance for EffectCore; furthermore, it is currently hard to figure out what functionality Effect has, since one can only see the MonadState/Reader etc. instances when looking at the source code.
  2. I would maybe even have expected some custom lifting function for this case; I would expect it to be rather common.

In all honesty: I don't really like the new 'Effect' setup; I tend to think of update essentially as a function 'model -> action -> model' that can additionally schedule some new actions. The previous type captured that perfectly, whereas the current type seems to push me towards update as something of type "action -> action" that may additionally modify the model. I find that unnatural: I would much rather have / pass / manipulate the model values explicitly.

noinia avatar May 25 '25 14:05 noinia

@noinia hey,

So yea there have been quite a few changes with 1.9, (and I haven’t made a migration guide just yet), including introducing real components (not just simulated via zoom), and Transition has now become Effect (and is RWS instead of StateT Writer), etc.

So the new Effect is still just the old Transition but new typed.

To fix the issues you’re experiencing we might be able to drop EffectCore or make it a type synonym, this will then give us all those instances for free (Transition was just a type synonym) like Zoom it seems.

So there’s a couple things we can do.

  1. just get it to compile. There’s no reason why your code shouldn’t just work with the new interface. So if you could wait for me to drop the newtype, it should work.

  2. Now that real components are here, you could attempt to port some stuff to them and using places where you call zoom as demarcations for new components (App is now Component)

Let me figure out the EffectCore situation. Should be able to today. Stay tuned.

Also there is Miso.Lens now, it might have everything you need except zoom. But I’d be open to adding that as a primitive if we can.

dmjio avatar May 25 '25 17:05 dmjio

@noinia also regarding the new Effect, just curious, how do you see it's like action -> action ? Is that when using scheduleIO or io ?

It's true in old Effect you could pure the model to return it updated. Now modifying the model is moreso done with lenses value += 1 or modify (and optionally not at all). We've made standard the old Transition interface, and now users don't need to call fromTransition either

We had some complaints from users that just wanted to emit an action and didn't like needing to pass the model along as well. By doing it this way we also remove the need to have an Id or NoOp constructor defined.

dmjio avatar May 26 '25 14:05 dmjio

Should be solved in #954 . Zoom for RWST should be available now.

dmjio avatar May 26 '25 15:05 dmjio

@noinia we have a matrix chat too if you'd like to join.

https://matrix.to/#/%23haskell-miso:matrix.org

dmjio avatar May 26 '25 21:05 dmjio

Thanks for the quick response/action! I think this should allow me to update my code :).

@noinia also regarding the new Effect, just curious, how do you see it's like action -> action ? Is that when using scheduleIO or io ?

Hmm, I think I mistakenly interpreted 'Effect model' as a monadic type returning a thing of. type 'action'. However, I guess that. was/is not accurate; it 'Effect model action' is a monadic type returning something of type ().

It's true in old Effect you could pure the model to return it updated. Now modifying the model is moreso done with lenses value += 1 or modify (and optionally not at all). We've made standard the old Transition interface, and now users don't need to call fromTransition either

I never used the Transition interface, but just directly constructed a thing of type 'Effect' using <#, noEff, and Effect (if I really needed to schedule multiple actions). (Or I used %%~ to apply some effectful computation to a particular field) of the model. In general I liked the fact that I had direct access to the thing of type model, and could explicitly construct an updated model out of it.

(I guess I can still use get(s) and modify in the current interface, but that still seems more clunky than before).

We had some complaints from users that just wanted to emit an action and didn't like needing to pass the model along as well.

I liked explicitly passing the (updated) model along; and didn't think 'm <# do .... ; pure MyAction' was a problem. But I guess it may be a matter of style.

noinia avatar May 27 '25 17:05 noinia

I don't necessarily disagree, the original Effect was designed around simplicity, using the <#, and #> combinators (although those are still technically supported w/ the new interface).

The main motivation for using Transition was to get access to the MonadState lenses (.=, etc.). If we could do this with the old Effect that might give us the best of both worlds. I can try to experiment with that, but my initial inclination tells me it isn't possible, but I will verify.

Transition was created by @basvandijk around 2016 iirc. now we just standardized on it essentially.

dmjio avatar May 27 '25 17:05 dmjio

@noinia, I would like to revisit this issue primarily because I believe monad transformers are a little too heavy for new users, and I've reconsidered what you've said above.

I liked explicitly passing the (updated) model along; and didn't think 'm <# do .... ; pure MyAction' was a problem. But I guess it may be a matter of style.

I've realized that using a type-indexed associated type-family might provide a way to accommodate both approaches (the RWS monad transformer, and the simple Effect type we have currently in 1.8.7), that you have also advocated for above.

The below code demonstrates how we'd index the new Component by a mode :: Mode type that would alter the signature of the update function to use the new Effect (1.9), and the other case uses the old (1.8.7) Effect, which is analogous to (model, [ Sink action -> JSM () ]).

-----------------------------------------------------------------------------
class Update (mode :: Mode) parent model action where
  type UpdateSignature mode parent model action :: Type
  convert
    :: Proxy mode
    -> model
    -> action
    -> UpdateSignature mode parent model action
    -> Effect parent model action
-----------------------------------------------------------------------------
data Mode = ADVANCED | SIMPLE
-----------------------------------------------------------------------------
instance Update ADVANCED parent model action where
  type (UpdateSignature ADVANCED parent model action) = action -> Effect parent model action
  convert Proxy _ action f = f action
-----------------------------------------------------------------------------
instance Update SIMPLE parent model action where
  type (UpdateSignature SIMPLE parent model action) = model -> action -> (model, [Sink action -> JSM ()])
  convert Proxy model action f =
    case f model action of
      (new, actions) -> do
         put new
         withSink $ \sink -> forM_ actions $ \k -> k sink
-----------------------------------------------------------------------------
-- | 'mode'
data Component (mode :: Mode) parent model action
  = Component
  { update :: UpdateSignature mode parent model action
     -- ^ this would expand into either the original `Effect` or `Transition`, without requiring the `fromTransition`, etc.
  }

where convert would be used internally to accommodate both approaches. This unfortunately introduces yet another type parameter to Component, but we could hide it through a clever use of type synoynms.

dmjio avatar Sep 04 '25 02:09 dmjio

Nice; I certainly prefer the 'SIMPLE' mode over the monad transformer mode. I don't think the extra type parameter is much of an issue (but maybe I don't exactly count as a beginner). Maybe one thing to double check is whether you can still write something along the lines of:

    data MyModel = MyModel { fieldA :: Int } 

    update myModel .. = myModel&fieldA %%~ flip (update subComponent) subAction   

I tend to find that a very convenient/compact way of implementing the update functions. In particular, I was wondering whether you want the result of UpdateSignature to be: (model, [Sink action -> JSM ()]), or something that is a Functor/Foldable/Traversable in model.

noinia avatar Sep 06 '25 07:09 noinia