Skip to content
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

Merged
merged 1 commit into from
Jun 1, 2021

Conversation

liskin
Copy link
Member

@liskin liskin commented May 17, 2021

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:

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 file

  • I updated the XMonad.Doc.Extending file (if appropriate)


(draft as it overrides CI instructions temporarily)

TODO

@liskin liskin force-pushed the pr/extensible-config branch from 3c18a13 to b0ca477 Compare May 17, 2021 18:01
@liskin liskin added this to the v0.17 milestone May 17, 2021
Copy link
Member

@slotThe slotThe left a 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)

Comment on lines 76 to 77
lookup :: Typeable a => XConfig l -> Maybe a
lookup c = let x = fromConfExt =<< typeRep x `M.lookup` extensibleConf c in x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines 80 to 85
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

Comment on lines 111 to 115
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines 120 to 124
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"

Copy link
Member

@slotThe slotThe May 18, 2021

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)

Copy link
Member Author

@liskin liskin May 18, 2021

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… ;-)

Copy link
Member

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 :>

Copy link
Member Author

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.

Copy link
Member

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 in base)

One could argue that that's just a mistake in base :P

liskin added a commit to xmonad/xmonad that referenced this pull request May 21, 2021
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
@liskin liskin mentioned this pull request May 21, 2021
4 tasks
@liskin liskin force-pushed the pr/extensible-config branch from feb8086 to ee35ad6 Compare May 21, 2021 18:07
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
@liskin liskin force-pushed the pr/extensible-config branch from ee35ad6 to f6b1e5d Compare June 1, 2021 18:07
@liskin liskin marked this pull request as ready for review June 1, 2021 18:07
@liskin liskin merged commit f6b1e5d into xmonad:master Jun 1, 2021
@liskin liskin deleted the pr/extensible-config branch June 1, 2021 18:13
colonelpanic8 pushed a commit to colonelpanic8/xmonad that referenced this pull request Jul 11, 2021
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants