Hans Krüger
Hans Krüger

Reputation: 108

Is there a canonical way of comparing/changing one/two records in haskell?

I want to compare two records in haskell, without defining each change in the datatype of the record with and each function of 2 datas for all of the elements of the record over and over.

I read about lens, but I could not find an example for that, and do not know where begin to read in the documentation.

Example, not working:

data TheState = TheState { number :: Int,
                           truth  :: Bool
                         }

initState = TheState 77 True

-- not working, example:
stateMaybe = fmap Just initState
--  result should be:
--  ANewStateType{ number = Just 77, truth = Just True}

The same way, I want to compare the 2 states:

state2 = TheState 78 True
-- not working, example
stateMaybe2 = someNewCompare initState state2
-- result should be:
-- ANewStateType{ number = Just 78, truth = Nothing}

Upvotes: 2

Views: 551

Answers (2)

danidiaz
danidiaz

Reputation: 27766

How to make a Functor out of a record. For that I have an answer: apply the function to > all of the items of the record.

I want to use the record as an heterogenous container / hashmap, where the names determine the values-types

While there's no "easy", direct way of doing this, it can be accomplished with several existing libraries.

This answer uses red-black-record library, which is itself built over the anonymous products of sop-core. "sop-core" allows each field in a product to be wrapped in a functor like Maybe and provides functions to manipulate fields uniformly. "red-black-record" inherits this, adding named fields and conversions from normal records.

To make TheState compatible with "red-black-record", we need to do the following:

{-# LANGUAGE DataKinds, FlexibleContexts, ScopedTypeVariables,
             DeriveGeneric, DeriveAnyClass,
             TypeApplications #-}

import GHC.Generics 
import Data.SOP 
import Data.SOP.NP (NP,cliftA2_NP) -- anonymous n-ary products
import Data.RBR (Record, -- generalized record type with fields wrapped in functors
                 I(..),  -- an identity functor for "simple" cases
                 Productlike, -- relates a map of types to its flattened list of types
                 ToRecord, toRecord, -- convert a normal record to its generalized form 
                 RecordCode, -- returns the map of types correspoding to a normal record
                 toNP, fromNP, -- convert generalized record to and from n-ary product
                 getField) -- access field from generalized record using TypeApplication

data TheState = TheState { number :: Int,
                           truth  :: Bool
                         } deriving (Generic,ToRecord)

We auto-derive the Generic instance that allows other code to introspect the structure of the datatype. This is needed by ToRecord, that allows conversion of normal records into their "generalized forms".

Now consider the following function:

compareRecords :: forall r flat. (ToRecord r, 
                                  Productlike '[] (RecordCode r) flat, 
                                  All Eq flat) 
               => r 
               -> r 
               -> Record Maybe (RecordCode r)
compareRecords state1 state2 = 
    let mapIIM :: forall a. Eq a => I a -> I a -> Maybe a
        mapIIM (I val1) (I val2) = if val1 /= val2 then Just val2
                                                   else Nothing
        resultNP :: NP Maybe flat
        resultNP = cliftA2_NP (Proxy @Eq) 
                              mapIIM 
                              (toNP (toRecord state1)) 
                              (toNP (toRecord state2)) 
     in fromNP resultNP

It compares two records whatsoever that have ToRecord r instances, and also a corresponding flattened list of types that all have Eq instances (the Productlike '[] (RecordCode r) flat and All Eq flat constraints).

First it converts the initial record arguments to their generalized forms with toRecord. These generalized forms are parameterized with an identity functor I because they come from "pure" values and there aren't any effects are play, yet.

The generalized record forms are in turn converted to n-ary products with toNP.

Then we can use the cliftA2_NP function from "sop-core" to compare accross all fields using their respective Eq instances. The function requires specifying the Eq constraint using a Proxy.

The only thing left to do is reconstructing a generalized record (this one parameterized by Maybe) using fromNP.

An example of use:

main :: IO ()
main = do
    let comparison = compareRecords (TheState 0 False) (TheState 0 True)
    print (getField @"number" comparison)
    print (getField @"truth" comparison)

getField is used to extract values from generalized records. The field name is given as a Symbol by way of -XTypeApplications.

Upvotes: 0

basile-henry
basile-henry

Reputation: 1365

As others have mentioned in comments, it's most likely easier to create a different record to hold the Maybe version of the fields and do the manual conversion. However there is a way to get the functor like mapping over your fields in a more automated way.

It's probably more involved than what you would want but it's possible to achieve using a pattern called Higher Kinded Data (HKD) and a library called barbies.

Here is a amazing blog post on the subject: https://chrispenner.ca/posts/hkd-options

And here is my attempt at using HKD on your specific example:

{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DeriveGeneric      #-}
{-# LANGUAGE FlexibleContexts   #-}

-- base
import           Data.Functor.Identity
import           GHC.Generics          (Generic)

-- barbie
import           Data.Barbie

type TheState = TheState_ Identity

data TheState_ f = TheState
  { number :: f Int
  , truth  :: f Bool
  } deriving (Generic, FunctorB)

initState :: TheState
initState = TheState (pure 77) (pure True)

stateMaybe :: TheState_ Maybe
stateMaybe = bmap (Just . runIdentity) initState

What is happening here, is that we are wrapping every field of the record in a custom f. We now get to choose what to parameterise TheState with in order to wrap every field. A normal record now has all of its fields wrapped in Identity. But you can have other versions of the record easily available as well. The bmap function let's you map your transformation from one type of TheState_ to another.

Honestly, the blog post will do a much better job at explaining this than I would. I find the subject very interesting, but I am still very new to it myself.

Hope this helped! :-)

Upvotes: 2

Related Questions