How to override a default value, via lenses, only if incoming value is not Nothing

こ雲淡風輕ζ 提交于 2019-12-11 05:47:27

问题


I'm basically trying to override a bunch of default values in a record only if the user-specific values are NOT Nothing. Is it possible to do it via lenses?

import qualified Data.Default as DD

instance DD.Def Nouns where
  def  = Nouns
    {
      -- default values for each field come here
    }

lookupHStore :: HStoreList -> Text -> Maybe Text

mkNounsFromHStoreList :: HStoreList -> Nouns
mkNounsFromHStoreList h = (DD.def Nouns)
  & depSingular .~ (lookupHStore h "dep_label_singular")
  -- ERROR: Won't compile because Text and (Maybe Text) don't match

回答1:


You could make your own combinator:

(~?) :: ASetter' s a -> Maybe a -> s -> s
s ~? Just a  = s .~ a
s ~? Nothing = id

Which you can use just like .~:

mkNounsFromHStoreList :: HStoreList -> Nouns
mkNounsFromHStoreList h =
  DD.def
    & myNoun1 ~? lookupHStore h "potato"
    & myNoun2 ~? lookupHStore h "cheese"



回答2:


This seems like a job for Alternative. Maybe's Alternative instance implements left-biased choice - its <|> chooses the first non-Nothing value.

import Control.Applicative
import Data.Semigroup

data Foo = Foo {
    bar :: Maybe Int,
    baz :: Maybe String
}

I'm going to implement a Semigroup instance for Foo which lifts <|> point-wise over the record fields. So the operation x <> y overrides the fields of y with the matching non-Nothing fields of x. (You can also use the First monoid, it does the same thing.)

instance Semigroup Foo where
    f1 <> f2 = Foo {
        bar = bar f1 <|> bar f2,
        baz = baz f1 <|> baz f2
    }

ghci> let defaultFoo = Foo { bar = Just 2, baz = Just "default" }
ghci> let overrides = Foo { bar = Just 8, baz = Nothing }
ghci> overrides <> defaultFoo
Foo {bar = Just 8, baz = Just "default"}

Note that you don't need lenses for this, although they might be able to help you make the implementation of (<>) a little terser.

When the user gives you a partially-filled-in Foo, you can fill in the rest of the fields by appending your default Foo.

fillInDefaults :: Foo -> Foo
fillInDefaults = (<> defaultFoo)

One fun thing you can do with this is factor the Maybe out of Foo's definition.

{-# LANGUAGE RankNTypes #-}

import Control.Applicative
import Data.Semigroup
import Data.Functor.Identity

data Foo f = Foo {
    bar :: f Int,
    baz :: f String
}

The Foo I originally wrote above is now equivalent to Foo Maybe. But now you can express invariants like "this Foo has all of its fields filled in" without duplicating Foo itself.

type PartialFoo = Foo Maybe  -- the old Foo
type TotalFoo = Foo Identity  -- a Foo with no missing values

The Semigroup instance, which only relied on Maybe's instance of Alternative, remains unchanged,

instance Alternative f => Semigroup (Foo f) where
    f1 <> f2 = Foo {
        bar = bar f1 <|> bar f2,
        baz = baz f1 <|> baz f2
    }

but you can now generalise defaultFoo to an arbitrary Applicative.

defaultFoo :: Applicative f => Foo f
defaultFoo = Foo { bar = pure 2, baz = pure "default" }

Now, with a little bit of Traversable-inspired categorical nonsense,

-- "higher order functors": functors from the category of endofunctors to the category of types
class HFunctor t where
    hmap :: (forall x. f x -> g x) -> t f -> t g

-- "higher order traversables",
-- about which I have written a follow up question: https://stackoverflow.com/q/44187945/7951906
class HFunctor t => HTraversable t where
    htraverse :: Applicative g => (forall x. f x -> g x) -> t f -> g (t Identity)
    htraverse eta = hsequence . hmap eta
    hsequence :: Applicative f => t f -> f (t Identity)
    hsequence = htraverse id

instance HFunctor Foo where
    hmap eta (Foo bar baz) = Foo (eta bar) (eta baz)
instance HTraversable Foo where
    htraverse eta (Foo bar baz) = liftA2 Foo (Identity <$> eta bar) (Identity <$> eta baz)

fillInDefaults can be adjusted to express the invariant that the resulting Foo is not missing any values.

fillInDefaults :: Alternative f => Foo f -> f TotalFoo
fillInDefaults = hsequence . (<> defaultFoo)

-- fromJust (unsafely) asserts that there aren't
-- any `Nothing`s in the output of `fillInDefaults`
fillInDefaults' :: PartialFoo -> TotalFoo
fillInDefaults' = fromJust . fillInDefaults

Probably overkill for what you need, but it's still pretty neat.




回答3:


Okay, I found a possible solution, but I'm still looking for a better one!

mkNounsFromHStoreList :: HStoreList -> Nouns
mkNounsFromHStoreList h = (DD.def Nouns)
  & depSingular %~ (overrideIfJust (lookupHStore h "dep_label_singular"))
  -- and more fields come here...
  where
    overrideIfJust val x = maybe x id val



回答4:


How about just using fromMaybe instead of creating an instance of Default?

EDIT: Since you seem to want to use the Default for other purposes as well:

λ > import Data.Default
λ > import Data.Maybe
λ > :t fromMaybe def
fromMaybe def :: Default a => Maybe a -> a

This seems to be what you are after.



来源:https://stackoverflow.com/questions/44184898/how-to-override-a-default-value-via-lenses-only-if-incoming-value-is-not-nothi

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!