Search code examples
haskellfunctional-programmingalgebraic-data-types

Is there a way in Haskell to express a point free function to edit the property of a data type?


I have the type:

data Cons = Cons {myprop :: String}

and later on I'm mapping over a list, setting the property to a different value:

fmap (\x -> x{myprop = ""}) mylist

Is there a point free way to express this anonymous function?


Solution

  • There isn't a point free way of doing this, but you can instead use the lens library (which comes with a whole basket of goodies):

    {-# LANGUAGE TemplateHaskell #-}
    import Control.Lens
    
    -- If you put a _ at the beginning of the field name lens can derive the methods for you
    data Cons = Cons { _myprop :: String } deriving (Eq, Show)
    -- Derives the myprop lens for you (works for all fields in the record)
    makeLenses ''Cons
    -- You can do this manually as
    -- myprop :: Functor f => (String -> f String) -> Cons -> f Cons
    -- myprop = lens _myprop (\c newProp -> c { _myprop = newProp })
    -- This actually lets you define the lenses for your type without depending on lens
    -- which is a bigger deal than you might think.  You can fully support a library and
    -- and its paradigms without depending on it at all.
    
    main = do
        let cs = map Cons ["a", "b", "c"]
            -- With the prefix set function
            test1 = fmap (set myprop "") cs
            -- Or with an operator
            test2 = fmap (myprop .~ "") cs
        print cs
        print test1
        print test2
    

    That "basket of goodies" that comes with lenses contains things like

    data Email = Email
        { _emailAccount :: String
        , _emailDomain :: String
        } deriving (Eq, Show)
    makeLenses ''Email
    
    data Person = Person
        { _personName :: String
        , _personAge :: Int
        , _personEmail :: Email
        } deriving (Eq, Show)
    makeLenses ''Person
    
    testPeople :: [Person]
    testPeople = [
        Person "A" 40 (Email "aaa" "gmail.com"),
        Person "B" 45 (Email "bbb" "email.com"),
        Person "C" 50 (Email "ccc" "outlook.com")]
    
    domains :: [Person] -> [String]
    domains ps = ps^..traverse.personEmail.emailDomain
    
    statefulFun :: MonadIO m => StateT Person m ()
    statefulFun = do
        liftIO $ putStrLn "Changing the name"
        personName .= "a different name"
        liftIO $ putStrLn "The name is changed!"
        personEmail.emailAccount %= map toUpper
    
    moreState :: MonadIO m => StateT Person m ()
    moreState = do
        personName .= "Foo"
        zoom personEmail $ do
            current <- get
            liftIO $ putStr "Current email is: "
            liftIO $ print current
            emailAccount .= "foo"
            emailDomain  .= "foo.com"
    
    main :: IO ()
    main = do
        finalState <- execStateT (moreState >> statefulFun) (head testPeople)
        print finalState
    

    As you can see, lenses look like the compose backwards (they actually don't, it's because they have a more general type that lets them do crazy things). There's nice ways of traversing complex data structures, optionally performing effects as you go, and plenty of operators for writing very imperative looking stateful code with what appears to be normal OOP method access with a dot. It lets you abstract common and complex patterns over large and complex data structures with little effort, once you start to grok them at least!