-
Notifications
You must be signed in to change notification settings - Fork 29
Which to cover first? Halogen Hooks or Halogen Components? #78
Comments
I'm glad you're considering this! I'm a little on the fence myself: I think it's potentially a good idea to start off with Hooks in the future, but there's a strong argument starting with components, too. I think Hooks are an easier introduction to Halogen. I think there's less to learn about (actions, queries, eval, etc.), especially if you wait to introduce child components. It's a more natural progression from "pure HTML function" to "stateful HTML function". But there's no way around components: they exist in Halogen, all existing resources still focus on components (because Hooks didn't exist), and ultimately a Hooks-based component is a component. Every Halogen developer will eventually have to learn about components to build an app because Halogen is built around them (things like Still, if Hooks do start to catch on, then it makes sense to introduce them first as the "standard" way to do things. We're not there yet and it probably makes sense to wait for a while for people to ask questions, problems to arise, and so on before switching over. But if, in a month or two, Hooks seem to be catching on and be the better approach for the default case, then this learning repository is a great way to encourage that style in the community. Your material is influential to how new Halogen devs write their code, and I think a change here would definitely impact how many people adopt Hooks. Compiler errorsAs you mentioned compiler errors, I just wanted to note that they aren't worse than regular Halogen components. There are some new possible compiler errors if you use Hooks in the wrong order in the body of the Hook definition, and in that way they could be subjectively "worse" -- but if you use the wrong field in state, or the input type is wrong, or something like that, the error spans will still be in the right place and the error shouldn't be more verbose. For example: type State = Int
initialState :: State
initialState = 0
type Input = String
myComponent :: forall q o m. H.Component q Input o m
myComponent = H.component \input -> Hooks.do
state /\ stateToken <- useState initialState
Hooks.pure do
HH.div
[ ]
[ HH.text $ show $ input + 1
, HH.text $ "hello" <> state
] in this component you'll get an error that |
I agree that we should wait until after Hooks have caught on and their tradeoffs are better known. As for the compiler errors... well... Having tried to implement some hooks myself, I'd say the compiler errors can be painful in very specific circumstances. However, this is similar to Halogen components: if you're new and put things in the wrong spot, it's hard to reason your way through it unless you already know what's causing it due to experience. I once got an error like this:
What was the issue? It wasn't the newtype UseX slots output {- m -} a = UseX --
derive instance newtypeUseX :: Newtype (UseX slots output m a) _ Removing the extra 'm' in the Another situation. When I use typed holes, |
That's a reasonable point, because this is something you might encounter while writing Hooks, though I wouldn't consider it a Hooks-specific compile-time error. Wouldn't you also encounter this if you wrote a newtype for something else and then used it in another function?
I'm curious about this one. In practice I think a Hook type like Fortunately, I believe only the slot type will cause this error. For example, you can wildcard all of the hook types in the Hooks library examples the same way you have done here, and you won't get an error. This error is likely confusing and frustrating, as you said. Unfortunately, it's an error you can run into in ordinary Halogen code, too, though it'll be encountered more often if Hooks are regularly written with the slot type in the Hook type. |
By the way -- if you continue encountering confusing type errors, I'd love if you shared them with me so I can add them to the Hooks documentation. That way people searching for the type error text can find the solution right away. |
Ok, I'll keep that in mind. Oh, actually, I think a better way to produce such documentation is to take a normal sensible hook implementation, change it on a simple way and see what kinds of errors arise, document the errors, revert those changes, and loop. That would probably cover most cases in a 15 minute window of time. |
So here's a fun fact. The below hook implementation compiles depending on which type I use. The only difference is this: -- using this type will compile successfully...
Hooked _ _ _ previousHooks (MyHook previousHooks) _
-- using this type will produce a compiler error...
Hook _ _ _ MyHook _
-- despite them being the same as far as I can tell:
type Hook slots ouput m nextHook a =
forall priorHooks. Hooked slots ouput m priorHooks (nextHook priorHooks) a When I use the
Code below. useEvent :: forall slots output m a hooks
. MonadEffect m
=> Hooked slots output m hooks
(UseRef (Maybe ( (HookM slots output m Unit -> HookM slots output m Unit)
-> a
-> HookM slots output m Unit
))
(UseRef (Maybe (HookM slots output m Unit))
hooks))
{ push :: a -> HookM slots output m Unit
, setCallback :: Maybe ((HookM slots output m Unit -> HookM slots output m Unit) -> a -> HookM slots output m Unit) -> HookM slots output m Unit
, unsubscribe :: HookM slots output m Unit
}
useEvent = Hooks.do
_ /\ unsubscribeRef <- useRef Nothing
_ /\ callbackRef <- useRef Nothing
let
push :: a -> HookM slots output m Unit
push value = do
mbCallback <- liftEffect $ Ref.read callbackRef
let
setupUnsubscribeCallback = \unsubscribe' -> do
mbUnsubscribe <- liftEffect $ Ref.read unsubscribeRef
case mbUnsubscribe of
Nothing -> do
liftEffect $ Ref.write (Just unsubscribe') unsubscribeRef
_ -> do
-- no need to store unsubscriber because
-- 1. it's already been stored
-- 2. no one has subscribed to this yet
pure unit
for_ mbCallback \callback -> do
callback setupUnsubscribeCallback value
setCallback callback =
liftEffect $ Ref.write callback callbackRef
unsubscribe = do
mbUnsubscribe <- liftEffect $ Ref.read unsubscribeRef
case mbUnsubscribe of
Just unsubscribe' -> do
unsubscribe'
liftEffect $ Ref.write Nothing unsubscribeRef
_ -> do
pure unit
Hooks.pure { push, setCallback, unsubscribe } |
-- using this type will compile successfully...
Hooked _ _ _ previousHooks (MyHook previousHooks) _
-- using this type will produce a compiler error...
Hook _ _ _ MyHook _
-- despite them being the same as far as I can tell:
type Hook slots ouput m nextHook a =
forall priorHooks. Hooked slots output m priorHooks (nextHook priorHooks) a The important part here is that every Hook takes, as its last argument, a type variable representing the next hook. All Hooks are thus of kind The entire chain of hooks you put together also needs to end up being of kind The -- this compiles
Hook slots output m (UseRef Int)
-- and is the same as this
Hooked slots output m priorHooks (UseRef Int priorHooks) But if you start building a chain of Hooks you can see that the type variable needs to be applied to the innermost Hook: -- this doesn't compile
Hook slots output m (UseRef Int (UseRef Int))
-- because it's the same as this
Hooked slots output m priorHooks (UseRef Int (UseRef Int) priorHooks)
-- instead of this, which does compile
Hooked slots output m priorHooks (UseRef Int (UseRef Int priorHooks)) You can see that in the full You can solve this by writing a newtype or type synonym: newtype UseEvent ps o m hooks =
UseEvent
(UseRef (Maybe (HookM ps o m Unit -> HookM ps o m Unit -> a -> HookM ps o m Unit))
(UseRef (Maybe (HookM ps o m Unit)) hooks))
-- now this compiles
Hook slots output m (UseEvent slots output m) ... This is definitely a confusing error to run into, and I should do a better job of explaining how this works (or potentially find a way to prevent this from occurring). However, if you follow the principle of making a new Hook type to represent your stack of Hooks then you won't run into this issue. |
That explains it. |
Another issue just posted in Slack: I've got a mystifying error with Hooks, here's the code: module Components.BookSearchModal whereimport Prelude
import Data.Tuple.Nested ((/\))
import Halogen.HTML as HH
import Halogen.Hooks as Hookscomponent =
Hooks.component \_ _ -> do
x /\ xId <- Hooks.useState 0
Hooks.pure
$ HH.div [] [] I'm getting this error about infinite types: [0] [1/1 InfiniteType] src/Component/BookSearchModal.purs:24:5
[0]
[0] v
[0] 24 Hooks.pure
[0] 25 $ HH.div [] []
[0] ^
[0]
[0] An infinite type was inferred for an expression:
[0]
[0] UseState Int t1
[0]
[0] while trying to match type t3
[0] with type UseState Int t1
[0] while checking that expression (apply pure) ((div []) [])
[0] has type Hooked t0 t1 (UseState Int t1) t2
[0] in value declaration component The issue was using |
While components are what Halogen actually understands, hooks are easier to read and use. I also think hooks don't require a template because they "just work." I'm not sure whether compiler errors are any worse than components.
Anyways, the question is, should this repo cover hooks first, which are likely easier to use, more powerful/flexible than components, and otherwise just better practically everywhere? Or should they be covered after components because that understanding is still the foundation of Halogen?
The text was updated successfully, but these errors were encountered: