getify / monio

The most powerful IO monad implementation in JS, possibly in any language!
http://monio.run
MIT License
1.05k stars 58 forks source link

Calling IO monad inside of another IO monad #27

Closed Djordjenp closed 2 years ago

Djordjenp commented 2 years ago

Hi Kyle, I was wondering does it make sense to be calling another IO monad inside of another like this

const addStyleIO = ([prop, val], element) =>
    IO(() => {
        element.style[prop] = val;
    })

const positionTooltip2 = (arc, tooltip) =>
    IO(() => arc.dataset.centroid.split(','))
        .chain(centroid => IO(() => {
            addStyleIO(['left', `calc(50% + ${centroid[0] }px)`], tooltip).run()
            addStyleIO(['top', `calc(50% + ${centroid[1] }px)`], tooltip).run()
            addStyleIO(['transform', 'translate(-50%, -50%)'], tooltip).run()
        })
        )
getify commented 2 years ago

Great question.

I'd probably do it first this way:

const positionTooltip2 = (arc, tooltip) =>
    IO.of( arc.dataset.centroid.split(',') )
    .chain(centroid =>
         addStyleIO(['left', `calc(50% + ${centroid[0] }px)`], tooltip)
         .chain(() => addStyleIO(['top', `calc(50% + ${centroid[1] }px)`], tooltip))
         .chain(() => addStyleIO(['transform', 'translate(-50%, -50%)'], tooltip))
    )

That works fine and doesn't rely on actually forcibly executing the inner IO. But it does still nest IOs, which is mildly distasteful. So instead I might consider doing this:

const addStyleIO = ( /* add this param ---> */ centroid, [prop, val], element) =>
    IO(() => {
        element.style[prop] = val;
        return centroid;     // <----------- also insert this line
    })

const positionTooltip2 = (arc, tooltip) =>
    IO.of( arc.dataset.centroid.split(',') )
    .chain(centroid => addStyleIO(centroid, ['left', `calc(50% + ${centroid[0] }px)`], tooltip))
    .chain(centroid => addStyleIO(centroid, ['top', `calc(50% + ${centroid[1] }px)`], tooltip))
    .chain(centroid => addStyleIO(centroid, ['transform', 'translate(-50%, -50%)'], tooltip))
)

That is a little intrusive/awkward to modify the definition of addStyleIO to forcibly continue the centroid context from chain(..) call to chain(..) call. It does work, though, and is an effective and not uncommon way of approaching things.

TBH, I think this situation is a decent justification for the "do-style" syntax over the "chain-style" syntax:

const positionTooltip2 = (arc, tooltip) => IO.do(function*(){
    var centroid = arc.dataset.centroid.split(',');
    yield addStyleIO(['left', `calc(50% + ${centroid[0] }px)`], tooltip);
    yield addStyleIO(['top', `calc(50% + ${centroid[1] }px)`], tooltip);
    yield addStyleIO(['transform', 'translate(-50%, -50%)'], tooltip);
})

If you can get over the function* and the yield overhead, I think this style is much cleaner and more readable for the task at hand. It creates a single scope for the multiple addStyleIOs to share (to access centroid), but everything is nicely flattened into a single IO (under the covers), so there's a clean un-nested vertical orientation to the IO operations.

Djordjenp commented 2 years ago

Thank you for answer Kyle, yeah I definitely see the advantage of using the generator form in this example.