-
-
Notifications
You must be signed in to change notification settings - Fork 279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
X.U.ExtensibleConf: New helper module for extensible config #547
Conversation
3c18a13
to
b0ca477
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool! 👍
I played around with the low-level primitives a bit because
lookup c = let x = fromConfExt =<< typeRep x `M.lookup` extensibleConf c in x
kind of broke my brain for a while; here's what came of it. I don't know if this is correct (the tests pass, at least :))
Needs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
and
theType :: forall a. Typeable a => TypeRep
theType = typeRep (Proxy :: Proxy a)
XMonad/Util/ExtensibleConf.hs
Outdated
lookup :: Typeable a => XConfig l -> Maybe a | ||
lookup c = let x = fromConfExt =<< typeRep x `M.lookup` extensibleConf c in x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lookup :: Typeable a => XConfig l -> Maybe a | |
lookup c = let x = fromConfExt =<< typeRep x `M.lookup` extensibleConf c in x | |
lookup :: forall a l. Typeable a => XConfig l -> Maybe a | |
lookup c = fromConfExt =<< theType @a `M.lookup` extensibleConf c |
XMonad/Util/ExtensibleConf.hs
Outdated
alter :: Typeable a => (Maybe a -> Maybe a) -> XConfig l -> XConfig l | ||
alter f c = c{ extensibleConf = M.alter f' t (extensibleConf c) } | ||
where | ||
f' :: Maybe ConfExtension -> Maybe ConfExtension | ||
f' = fmap ConfExtension . f . (>>= fromConfExt) | ||
t = typeRep (f undefined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alter :: Typeable a => (Maybe a -> Maybe a) -> XConfig l -> XConfig l | |
alter f c = c{ extensibleConf = M.alter f' t (extensibleConf c) } | |
where | |
f' :: Maybe ConfExtension -> Maybe ConfExtension | |
f' = fmap ConfExtension . f . (>>= fromConfExt) | |
t = typeRep (f undefined) | |
alter :: forall a l. Typeable a => (Maybe a -> Maybe a) -> XConfig l -> XConfig l | |
... | |
t = theType @a |
(perhaps we can even inline t
now)
XMonad/Util/ExtensibleConf.hs
Outdated
once :: (Semigroup a, Typeable a) | ||
=> a -- ^ configuration to add | ||
-> (XConfig l -> XConfig l) -- ^ 'XConfig' modification done only once | ||
-> XConfig l -> XConfig l | ||
once x f c = add x $ maybe f (const id) (lookup c `asTypeOf` Just x) c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
once :: (Semigroup a, Typeable a) | |
=> a -- ^ configuration to add | |
-> (XConfig l -> XConfig l) -- ^ 'XConfig' modification done only once | |
-> XConfig l -> XConfig l | |
once x f c = add x $ maybe f (const id) (lookup c `asTypeOf` Just x) c | |
once :: forall a l. (Semigroup a, Typeable a) | |
... | |
once x f c = add x $ maybe f (const id) (lookup @a c) c |
XMonad/Util/ExtensibleConf.hs
Outdated
onceM :: (Applicative m, Semigroup a, Typeable a) | ||
=> a -- ^ configuration to add | ||
-> (XConfig l -> m (XConfig l)) -- ^ 'XConfig' modification done only once | ||
-> XConfig l -> m (XConfig l) | ||
onceM x f c = add x <$> maybe f (const pure) (lookup c `asTypeOf` Just x) c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onceM :: (Applicative m, Semigroup a, Typeable a) | |
=> a -- ^ configuration to add | |
-> (XConfig l -> m (XConfig l)) -- ^ 'XConfig' modification done only once | |
-> XConfig l -> m (XConfig l) | |
onceM x f c = add x <$> maybe f (const pure) (lookup c `asTypeOf` Just x) c | |
onceM :: forall a m l. (Applicative m, Semigroup a, Typeable a) | |
... | |
onceM x f c = add x <$> maybe f (const pure) (lookup @a c) c |
XC.lookup (XC.add "a" def) `shouldBe` Just "a" | ||
specify "lookup . add . add" $ | ||
XC.lookup (XC.add "b" (XC.add "a" def)) `shouldBe` Just "ab" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests don't catch the (stupid) error of using the same type representation for everything. I'd add something like
let a = XC.add @[Int] [1] def
b = XC.add @String "b" a
specify "lookup @String . add @String . add @[Int]" $
XC.lookup @String b `shouldBe` Just "b"
specify "lookup @String . add @String . add @[Int]" $
XC.lookup @[Int] b `shouldBe` Just [1]
here (needs TypeApplications
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a similar test: feb8086
I can't decide about the rest. :-/
Some people seem to have strong opinions, but I don't. I wish it was possible to write once
without messing with the types. Then I'd just say that the low-level code won't be modified anyway and push whatever a dice roll tells me to push. But as this abstraction leaks into higher levels, it may be worth debating a little bit. Just a little bit though… ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I suppose my opinion should be obvious—I think the version with TypeApplications
is much more readable. I will, however, gladly open this up to some productive bike-shedding :>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So how about this:
I pushed ee35ad6, which uses TypeApplications, but doesn't define theType
(if Haskell gods wanted us to have forall a. Typeable a => TypeRep
, they'd have put it in base
). If people oppose that impl, they're free to comment constructively here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me 👍
(if Haskell gods wanted us to have
forall a. Typeable a => TypeRep
, they'd have put it inbase
)
One could argue that that's just a mistake in base
:P
It's often difficult to make contrib modules work together. When one depends on a functionality of another, it is often necessary to expose lots of low-level functions and hooks and have the user combine these into a complex configuration that works. This is error-prone, and arguably a bad UX in general. This commit presents a simple solution to that problem inspired by "extensible state": extensible config. It allows contrib modules to store custom configuration values inside XConfig. This lets them create custom hooks, ensure they hook into xmonad core only once, and possibly other use cases I haven't thought of yet. For more, see the related pull request to xmonad-contrib. Related: xmonad/xmonad-contrib#547
feb8086
to
ee35ad6
Compare
It's often difficult to make contrib modules work together. When one depends on a functionality of another, it is often necessary to expose lots of low-level functions and hooks and have the user combine these into a complex configuration that works. This is error-prone, and arguably a bad UX in general. This commit presents a simple solution to that problem inspired by "extensible state": extensible config. It allows contrib modules to store custom configuration values inside XConfig. This lets them create custom hooks, ensure they hook into xmonad core only once, and possibly other use cases I haven't thought of yet. This requires changes to xmonad core: xmonad/xmonad#294 A couple examples of what this gives us: * [X.H.RescreenHook](xmonad#460) can be made safe to apply multiple times, making it composable and usable in other contrib modules like X.H.StatusBar * `withSB` from X.H.StatusBar can also be made safe to apply multiple times, and we can even provide an API [similar to what we had before](https://hackage.haskell.org/package/xmonad-contrib-0.16/docs/XMonad-Hooks-DynamicLog.html#v:statusBar) if we want (probably not, consistency with the new dynamic status bars of xmonad#463 is more important) * The [X.H.EwmhDesktops refactor](xmonad#399) can possibly be made without breaking the `ewmh`/`ewmhFullscreen` API. And we will finally be able to have composable EWMH hooks. Related: xmonad/xmonad#294
ee35ad6
to
f6b1e5d
Compare
It's often difficult to make contrib modules work together. When one depends on a functionality of another, it is often necessary to expose lots of low-level functions and hooks and have the user combine these into a complex configuration that works. This is error-prone, and arguably a bad UX in general. This commit presents a simple solution to that problem inspired by "extensible state": extensible config. It allows contrib modules to store custom configuration values inside XConfig. This lets them create custom hooks, ensure they hook into xmonad core only once, and possibly other use cases I haven't thought of yet. For more, see the related pull request to xmonad-contrib. Related: xmonad/xmonad-contrib#547
Description
It's often difficult to make contrib modules work together. When one depends on a functionality of another, it is often necessary to expose lots of low-level functions and hooks and have the user combine these into a complex configuration that works. This is error-prone, and arguably a bad UX in general.
This commit presents a simple solution to that problem inspired by "extensible state": extensible config. It allows contrib modules to store custom configuration values inside XConfig. This lets them create custom hooks, ensure they hook into xmonad core only once, and possibly other use cases I haven't thought of yet.
This requires changes to xmonad core: xmonad/xmonad#294
A couple examples of what this gives us:
X.H.RescreenHook can be made safe to apply multiple times, making it composable and usable in other contrib modules like X.H.StatusBar
withSB
from X.H.StatusBar can also be made safe to apply multiple times, and we can even provide an API similar to what we had before if we want (probably not, consistency with the new dynamic status bars of Dynamic Status Bars support for XMonad.Hooks.StatusBar #463 is more important)The X.H.EwmhDesktops refactor can possibly be made without breaking the
ewmh
/ewmhFullscreen
API. And we will finally be able to have composable EWMH hooks.Related: xmonad/xmonad#294
Checklist
I've read CONTRIBUTING.md
I've considered how to best test these changes (property, unit, manually, ...) and concluded:
There's a unit test. As the code deals with dynamically typed data, I actually needed the test to get it right. :-)
I updated the
CHANGES.md
fileI updated the
XMonad.Doc.Extending
file (if appropriate)(draft as it overrides CI instructions temporarily)
TODO