Hyperapp: Built-in support for HOA: Should it stay or should it go? 馃幎

Created on 10 Oct 2017  路  19Comments  路  Source: jorgebucaran/hyperapp

Should we keep built-in support for HOAs (higher order apps) or shall we encourage you to roll your own DIY solution? For example using a right to left function reducer like compose or good old f(g(..)).

The tradeoff is more flexibility & less bytes for one less selling point.


Some facts about HOAs:

  • 99% users will never need to author a HOA.
  • You don't even need to use a HOA, but some of you may want to in order to facilitate debugging.
  • HOAs are ought to be used primarily for adding debugging functionality to your apps, enable logging of actions / state updates, add Developer Console support (devtools), TimeTravel, HMR, bypass the VDOM to target a different renderer, e.g., a terminal. Other valid uses, but not strongly encouraged include augmenting Hyperapp "core features", e.g., introducing new app props to enable pre-wired component views, etc.
  • HOA is not a unique Hyperapp cooncept. You can create a higher order function of any function in the universe. Hyperapp does go out of its way to make slightly more convenient for you if you want to author a HOA, but we are not bending the laws of physics or being too unique.
  • You can do and abuse HOAs as much as you want with this feature built-in or not (in case we decide to remove it).

/cc @pspeter3 @okwolf @zaceno

Community Discussion

Most helpful comment

Now with init and modules landed in core and with the recent realization that the built-in support for authoring a HOA is not particularly that helpful, I propose we remove this from core and encourage users to DIY! 馃敟

All 19 comments

@JorgeBucaran would you be opposed to publishing an official @hyperapp/compose so less wheel reinventing would occur?

Probably, I'll keep that in mind. Let's see what everyone else thinks and where this issues leads us.

So, this issue was brought up from this discussion.

After realizing that in order to use multiple HOAs each HOA needs to be coded with care as to not break the middleware chain*, @pspeter3 commented:

I feel like the HOA abstraction is great but the API feels really awkward when you need to consider all the other higher order apps.

...and...

That's exactly what I'm thinking. It seems even less necessary now that there are modules.


... to not break the middleware chain involves returning (from your higher order app function) in a similar way to how app.js itself handles HOAs:

const myHOA = app => props => typeof props === "function" 
  ? props(app) 
  : enhanceTheApp(props)

Question: does HOAs need access to the app()'s returned actions/modify app()'s return type?

If not, then we could instead support HOP/props enhancer: (appProps) => enhance(appProps). This would be easily composable and we may want to keep support for that in core?

@Mytrill They may need to access the actions returned by app() as well as enhance props and create a closure. Can you show an example of what you are saying?

The main proposal were to compose HOA in a curried way, is this really prerequisite to this feature ?

I mean we are not stuck at the use of this curried way app(HOA1)(HOA2)(HOA3)({ ... }).
Maybe it would be good to compose from an array of HOA app([HOA1, HOA2, HOA3])({ ... }).
HOA will still need to be aware of the returned value of the previous HOA.

@Swizz Reminds me of mixins! 馃槈

I like @Swizz's idea of an array, it makes it easier to dynamically add HOAs (e.g. depending on the environment)

Here is something that could work if we want to support it in core and access to app()'s result:

// general pattern
const doSomethingWithAppResult = actions => actions
const doSomethingWithProps = props => props

const AppEnhancer = next => props => {
  // returns the enhanced app
  return doSomethingWithAppResult(
    // calls the next HOA/app enhancer
    next(
      // enhance the props
      doSomethingWithProps(props)
    )
  )
}

// example hoas
const hoa1 = next => props => {
  console.log('HOA 1 received props: ' + JSON.stringify(props))
  const result = next({ ...props, hoa1: true })
  console.log('HOA 1 returns app: ' + JSON.stringify(result))
  return result
}

const hoa2 = next => props => {
  console.log('HOA 2 received props: ' + JSON.stringify(props))
  const result = next({ ...props, hoa2: true })
  console.log('HOA 2 returns app: ' + JSON.stringify(result))
  return result
}

// implementation of app()
function app(appProps) {
  if (Array.isArray(appProps)) {
    return appProps.reduce(function(prev, curr) {
      return curr(prev)
    }, app)
  }
  // rest of app()'s implementation...
  // just wrap the props for this example
  return { props: appProps }
}

console.log('Final app: ' + JSON.stringify(app([hoa1, hoa2])({})))

The snippet above logs:

HOA 2 received props: {}
HOA 1 received props: {"hoa2":true}
HOA 1 returns app: {"props":{"hoa2":true,"hoa1":true}}
HOA 2 returns app: {"props":{"hoa2":true,"hoa1":true}}
Final app: {"props":{"hoa2":true,"hoa1":true}}

We could also reverse the array/use reduceRight so they are called in the intuitive order...

@JorgeBucaran You meant ?

app({
  ehancers: [HOA1, HOA2, HO3]
})

Looking at a basic example of what a HOA actually is supposed to do,

function hoa(app) {
  return props => {
    return app(enhance(props))

    function enhance(props) {
      // Enhance your props here.
    }
  }
}

We're not doing anything with app, we are manipulating the props we pass in.
It feels quite logical to do that _before_ calling app, so that your props are all good and ready by then.

Basically, one or many function (props) functions are what people really need.
And manipulating an object is like the simplest thing you can do.

So I feel that the need for app in all of this is a cause for initial confusion more than anything,
and forces the developer to have to consider something they shouldn't have to,

const myHOA = app => props => typeof props === "function" 
  ? props(app) 
  : enhanceTheApp(props)

Documenting that to enhance the application, all you need is a function (props) seems like the easiest thing to do both for Hyperapp but also users of Hyperapp?

Now with init and modules landed in core and with the recent realization that the built-in support for authoring a HOA is not particularly that helpful, I propose we remove this from core and encourage users to DIY! 馃敟

@johanalkstal in this case, you are removing the ability for HOAs to enhance the value returned by app()

@Mytrill I was just using the examples provided by @JorgeBucaran to explain what HOAs are.
Are those examples incorrect?

Also, why would you want to manipulate the returned actions from app? I can't see why?
Or did I missunderstand you?

Modifying an object before passing it to a function (in this case, app) is a very common thing to do (at least for me, but I would think for most people). It's so common, in fact, that I arrived at the same solution as modules, but called it components instead. So perhaps we won't need HOAs in the next release.

@Mytrill All I am saying is let's compose HOAs outside core, not veto the bill 馃槈

Instead of app(logger)({ ... }):

logger(app)({ ... })

I agree that we take out the support for passing a HOA into app, and let people compose their enhanced apps however is appropriate for their use case. Because use cases range from simple (just using one HOA or none always) to complex (multiple HOA, some must be switched on or off depending on NODE_ENV, etc)

And as long as HOA can just be regular functions with no special tricks built in, everyone will know how to use them and have their own preferred way of dealing with them.

Besides, I think the most common scenario is zero HOA anyway, as @JorgeBucaran pointed out.

@zaceno I rewrote the HOA tests and didn't need to touch the implementation of the HOA functions (as you'd expect) and the difference is only in how you apply it to app. No need for compose and the result is more functional buddy. Having said that, a utility function to help compose multiple HOAs in particular ways as you described could be handy.

B(A(app))({
  state: {
    value: 0
  },
  init(state) {
    expect(state).toEqual({
      value: 2
    })

    done()
  }
})

Ah ok, got it, thanks!
I'm all for removing the support then!

I'm in favor as well. I think we can have docs like React does.

Changed my mind too. The following A(B(app))({ ... }) is better for users, community developers and remove complexity in core.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dmitrykurmanov picture dmitrykurmanov  路  3Comments

jacobtipp picture jacobtipp  路  3Comments

dwknippers picture dwknippers  路  3Comments

zaceno picture zaceno  路  3Comments

jorgebucaran picture jorgebucaran  路  3Comments