Config Files and monoids

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

Config Files and monoids

Olaf Klinke
Dear cafe,

a recent post here [1] mentioned that configurations, such as the ones read from a config file, can be given Monoid instances, where mempty is the empty or default configuration and mappend merges two partial configurations, producing a more complete one. The vgrep package explicitly does this, for instance. Although the ConfigParser type from the ConfigFile package has a binary 'merge' operation, it does define neither a Monoid not a Semigroup instance.

I'm struggling to make the concept of monoidal configuration work when there is no sensible default configuration. Suppose my configuration type is

data Config = Config {foo :: Bool, bar :: Int}

with no reasonable default, e.g.

emptyConfig = Config {
  foo = error "you did not specify option foo",
  bar = error "you did not specify option bar"
  }

Some configuration monoids seem to have the second operand override the first, or the other way around. However, I wish that when
cfg1 = emptyConfig {foo = True}
cfg2 = emptyConfig {bar = 4}
then cfg1 <> cfg2 == Config {foo = True, bar = 4}.

So it seems that for mappend to work as intended one needs a terminating function that tells me if a record field is already defined, e.g. when all fields are Maybes. Vgrep.Environment.Config.Monoid does it this way. My solution so far was to resort to the monoid of endofunctions (as the getflag package does), that is, define

cfg1, cfg2 :: Config -> Config
cfg1 = \cfg -> cfg {foo = True}
cfg2 = \cfg -> cfg {bar = 4}

And then build (cfg1.cfg2) emptyConfig. (Alternatively, one might structure these as lenses instead of endofunctions, see e.g. Data.Monoid.Endo.Fold in the endo package.)
Thus I arrived at

class Config cfg where
  emptyConfig   :: cfg -- may contain some defaults
  configOptions :: [Parser (cfg -> cfg)]

Do you think every other concept of configuration parsing can be cast into this typeclass?
-- Olaf

[1] https://mail.haskell.org/pipermail/haskell-cafe/2018-May/129063.html
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Config Files and monoids

Matt
For a "fully general" approach, the problem is well expressed by the "higher kinded data" pattern: http://reasonablypolymorphic.com/blog/higher-kinded-data/

A `Config f = Config { configFoo :: f Foo, ... }` type would use either the First or Last monoids, depending on if you want earlier updates to take precedence over later ones. Then, you would get a `Config First` from your CLI parser, a `Config First` from your environment variable parser, and a `Config First` from your config file parser. After `mappend`ing them all together, you'd use a `gtraverse` function with a signature like: `Config First -> Either [Text] (Config Identity)` -- you'd either have a list of all fields that were missing, or a complete Config.

Matt Parsons

On Thu, May 24, 2018 at 3:00 PM, Olaf Klinke <[hidden email]> wrote:
Dear cafe,

a recent post here [1] mentioned that configurations, such as the ones read from a config file, can be given Monoid instances, where mempty is the empty or default configuration and mappend merges two partial configurations, producing a more complete one. The vgrep package explicitly does this, for instance. Although the ConfigParser type from the ConfigFile package has a binary 'merge' operation, it does define neither a Monoid not a Semigroup instance.

I'm struggling to make the concept of monoidal configuration work when there is no sensible default configuration. Suppose my configuration type is

data Config = Config {foo :: Bool, bar :: Int}

with no reasonable default, e.g.

emptyConfig = Config {
  foo = error "you did not specify option foo",
  bar = error "you did not specify option bar"
  }

Some configuration monoids seem to have the second operand override the first, or the other way around. However, I wish that when
cfg1 = emptyConfig {foo = True}
cfg2 = emptyConfig {bar = 4}
then cfg1 <> cfg2 == Config {foo = True, bar = 4}.

So it seems that for mappend to work as intended one needs a terminating function that tells me if a record field is already defined, e.g. when all fields are Maybes. Vgrep.Environment.Config.Monoid does it this way. My solution so far was to resort to the monoid of endofunctions (as the getflag package does), that is, define

cfg1, cfg2 :: Config -> Config
cfg1 = \cfg -> cfg {foo = True}
cfg2 = \cfg -> cfg {bar = 4}

And then build (cfg1.cfg2) emptyConfig. (Alternatively, one might structure these as lenses instead of endofunctions, see e.g. Data.Monoid.Endo.Fold in the endo package.)
Thus I arrived at

class Config cfg where
  emptyConfig   :: cfg -- may contain some defaults
  configOptions :: [Parser (cfg -> cfg)]

Do you think every other concept of configuration parsing can be cast into this typeclass?
-- Olaf

[1] https://mail.haskell.org/pipermail/haskell-cafe/2018-May/129063.html
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Config Files and monoids

lennart spitzner-2
Thanks matt for this pointer. I have also used the higher-kinded approach in the past, but using a slightly different abstraction. This is introduced in a very recent post [1]. I think this gives you the custom `gtraverse` you describe essentially for free (I assume you'd still need to write that, given that it has a custom type, right?). However it does not use the type family trick to avoid the `Identity` wrappers.

The examples in my post still mostly assume that there is a default config, but I think you could work around this. This would involve the `CZipWithM` class instead of just `CZipWith`: The simple path would be

  cTraverse (fmap Identity) :: MyConfig Option -> Option (MyConfig Identity)

which roughly corresponds to `gvalidate` from the "Higher-Kinded Data" post. The downside is that a Nothing result would not tell you which field(s) were missing. To fix that, you could define a static value of type `MyConfig (Const String)` that adds a value-level name to each field, and use `cZipWithM` to produce a `Either String (MyConfig Identity)` or perhaps even `Either [String] (MyConfig Identity)` by using the right traversal monad.

Hope this helps.

-- lennart

[1] http://hexagoxel.de/postsforpublish/posts/2018-05-24-program-configuration.html


On 25/05/18 00:03, Matt wrote:

> For a "fully general" approach, the problem is well expressed by the
> "higher kinded data" pattern:
> http://reasonablypolymorphic.com/blog/higher-kinded-data/
>
> A `Config f = Config { configFoo :: f Foo, ... }` type would use either the
> First or Last monoids, depending on if you want earlier updates to take
> precedence over later ones. Then, you would get a `Config First` from your
> CLI parser, a `Config First` from your environment variable parser, and a
> `Config First` from your config file parser. After `mappend`ing them all
> together, you'd use a `gtraverse` function with a signature like: `Config
> First -> Either [Text] (Config Identity)` -- you'd either have a list of
> all fields that were missing, or a complete Config.
>
> Matt Parsons
>
> On Thu, May 24, 2018 at 3:00 PM, Olaf Klinke <[hidden email]> wrote:
>
>> Dear cafe,
>>
>> a recent post here [1] mentioned that configurations, such as the ones
>> read from a config file, can be given Monoid instances, where mempty is the
>> empty or default configuration and mappend merges two partial
>> configurations, producing a more complete one. The vgrep package explicitly
>> does this, for instance. Although the ConfigParser type from the ConfigFile
>> package has a binary 'merge' operation, it does define neither a Monoid not
>> a Semigroup instance.
>>
>> I'm struggling to make the concept of monoidal configuration work when
>> there is no sensible default configuration. Suppose my configuration type
>> is
>>
>> data Config = Config {foo :: Bool, bar :: Int}
>>
>> with no reasonable default, e.g.
>>
>> emptyConfig = Config {
>>   foo = error "you did not specify option foo",
>>   bar = error "you did not specify option bar"
>>   }
>>
>> Some configuration monoids seem to have the second operand override the
>> first, or the other way around. However, I wish that when
>> cfg1 = emptyConfig {foo = True}
>> cfg2 = emptyConfig {bar = 4}
>> then cfg1 <> cfg2 == Config {foo = True, bar = 4}.
>>
>> So it seems that for mappend to work as intended one needs a terminating
>> function that tells me if a record field is already defined, e.g. when all
>> fields are Maybes. Vgrep.Environment.Config.Monoid does it this way. My
>> solution so far was to resort to the monoid of endofunctions (as the
>> getflag package does), that is, define
>>
>> cfg1, cfg2 :: Config -> Config
>> cfg1 = \cfg -> cfg {foo = True}
>> cfg2 = \cfg -> cfg {bar = 4}
>>
>> And then build (cfg1.cfg2) emptyConfig. (Alternatively, one might
>> structure these as lenses instead of endofunctions, see e.g.
>> Data.Monoid.Endo.Fold in the endo package.)
>> Thus I arrived at
>>
>> class Config cfg where
>>   emptyConfig   :: cfg -- may contain some defaults
>>   configOptions :: [Parser (cfg -> cfg)]
>>
>> Do you think every other concept of configuration parsing can be cast into
>> this typeclass?
>> -- Olaf
>>
>> [1] https://mail.haskell.org/pipermail/haskell-cafe/2018-May/129063.html
>> _______________________________________________
>> Haskell-Cafe mailing list
>> To (un)subscribe, modify options or view archives go to:
>> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>> Only members subscribed via the mailman list are allowed to post.
>
>
>
> _______________________________________________
> Haskell-Cafe mailing list
> To (un)subscribe, modify options or view archives go to:
> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
> Only members subscribed via the mailman list are allowed to post.
>

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Config Files and monoids

Douglas McClean
To get the static record of names you need to make good error messages for the missing things, I think it should also be possible to write a GHC.Generics function with an end result like:

gNames :: (GNames r) => r (Const String)

by digging through the metadata for each field in the record type r, extracting the name, using Const . fromString, and packaging it up in the record?
But exactly how the Generic1 machinery for this works is escaping me at this hour. Perhaps someone can help fill in the details?

On Thu, May 24, 2018 at 6:45 PM, lennart spitzner <[hidden email]> wrote:
Thanks matt for this pointer. I have also used the higher-kinded approach in the past, but using a slightly different abstraction. This is introduced in a very recent post [1]. I think this gives you the custom `gtraverse` you describe essentially for free (I assume you'd still need to write that, given that it has a custom type, right?). However it does not use the type family trick to avoid the `Identity` wrappers.

The examples in my post still mostly assume that there is a default config, but I think you could work around this. This would involve the `CZipWithM` class instead of just `CZipWith`: The simple path would be

  cTraverse (fmap Identity) :: MyConfig Option -> Option (MyConfig Identity)

which roughly corresponds to `gvalidate` from the "Higher-Kinded Data" post. The downside is that a Nothing result would not tell you which field(s) were missing. To fix that, you could define a static value of type `MyConfig (Const String)` that adds a value-level name to each field, and use `cZipWithM` to produce a `Either String (MyConfig Identity)` or perhaps even `Either [String] (MyConfig Identity)` by using the right traversal monad.

Hope this helps.

-- lennart

[1] http://hexagoxel.de/postsforpublish/posts/2018-05-24-program-configuration.html


On 25/05/18 00:03, Matt wrote:
> For a "fully general" approach, the problem is well expressed by the
> "higher kinded data" pattern:
> http://reasonablypolymorphic.com/blog/higher-kinded-data/
>
> A `Config f = Config { configFoo :: f Foo, ... }` type would use either the
> First or Last monoids, depending on if you want earlier updates to take
> precedence over later ones. Then, you would get a `Config First` from your
> CLI parser, a `Config First` from your environment variable parser, and a
> `Config First` from your config file parser. After `mappend`ing them all
> together, you'd use a `gtraverse` function with a signature like: `Config
> First -> Either [Text] (Config Identity)` -- you'd either have a list of
> all fields that were missing, or a complete Config.
>
> Matt Parsons
>
> On Thu, May 24, 2018 at 3:00 PM, Olaf Klinke <[hidden email]> wrote:
>
>> Dear cafe,
>>
>> a recent post here [1] mentioned that configurations, such as the ones
>> read from a config file, can be given Monoid instances, where mempty is the
>> empty or default configuration and mappend merges two partial
>> configurations, producing a more complete one. The vgrep package explicitly
>> does this, for instance. Although the ConfigParser type from the ConfigFile
>> package has a binary 'merge' operation, it does define neither a Monoid not
>> a Semigroup instance.
>>
>> I'm struggling to make the concept of monoidal configuration work when
>> there is no sensible default configuration. Suppose my configuration type
>> is
>>
>> data Config = Config {foo :: Bool, bar :: Int}
>>
>> with no reasonable default, e.g.
>>
>> emptyConfig = Config {
>>   foo = error "you did not specify option foo",
>>   bar = error "you did not specify option bar"
>>   }
>>
>> Some configuration monoids seem to have the second operand override the
>> first, or the other way around. However, I wish that when
>> cfg1 = emptyConfig {foo = True}
>> cfg2 = emptyConfig {bar = 4}
>> then cfg1 <> cfg2 == Config {foo = True, bar = 4}.
>>
>> So it seems that for mappend to work as intended one needs a terminating
>> function that tells me if a record field is already defined, e.g. when all
>> fields are Maybes. Vgrep.Environment.Config.Monoid does it this way. My
>> solution so far was to resort to the monoid of endofunctions (as the
>> getflag package does), that is, define
>>
>> cfg1, cfg2 :: Config -> Config
>> cfg1 = \cfg -> cfg {foo = True}
>> cfg2 = \cfg -> cfg {bar = 4}
>>
>> And then build (cfg1.cfg2) emptyConfig. (Alternatively, one might
>> structure these as lenses instead of endofunctions, see e.g.
>> Data.Monoid.Endo.Fold in the endo package.)
>> Thus I arrived at
>>
>> class Config cfg where
>>   emptyConfig   :: cfg -- may contain some defaults
>>   configOptions :: [Parser (cfg -> cfg)]
>>
>> Do you think every other concept of configuration parsing can be cast into
>> this typeclass?
>> -- Olaf
>>
>> [1] https://mail.haskell.org/pipermail/haskell-cafe/2018-May/129063.html
>> _______________________________________________
>> Haskell-Cafe mailing list
>> To (un)subscribe, modify options or view archives go to:
>> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>> Only members subscribed via the mailman list are allowed to post.
>
>
>
> _______________________________________________
> Haskell-Cafe mailing list
> To (un)subscribe, modify options or view archives go to:
> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
> Only members subscribed via the mailman list are allowed to post.
>

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.



--
J. Douglas McClean

(781) 561-5540 (cell)

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Config Files and monoids

Sven Panne-2
In reply to this post by Olaf Klinke
2018-05-24 23:00 GMT+02:00 Olaf Klinke <[hidden email]>:
[...] I'm struggling to make the concept of monoidal configuration work when there is no sensible default configuration. Suppose my configuration type is

data Config = Config {foo :: Bool, bar :: Int}

with no reasonable default, e.g.

emptyConfig = Config {
  foo = error "you did not specify option foo",
  bar = error "you did not specify option bar"
  }
[...]

I find the approach in https://medium.com/@jonathangfischoff/the-partial-options-monoid-pattern-31914a71fc67 quite straightforward, without any need for higher-kinded stuff, generics or lenses: Just distinguish between partial and non-partial options, and make a Monoid instance only for the partial ones.

Cheers,
   S.
 

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Config Files and monoids

Olaf Klinke
In reply to this post by Douglas McClean
Thanks to Lennart, Matt, Douglas and Sven for the links to the intetesting posts. Once you know it, the higher-kinded approach seems a natural solution to the problem at hand. I might even begin to like type families on account of their ability to eliminate the Identity constructor in `Option Identity`.

I shall re-work my option parser type class an see how much easier it gets.

Olaf

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.