George
George

Reputation: 7327

Looking up keys (parsed as String) from a Haskell Record

Suppose I have a Haskell record like

data HaskellRecord = HaskellRecord {
  _key1 :: Maybe String
, _key2 :: Maybe String
, _key3 :: Maybe String
}

Is there a way to construct a function

getKey :: HaskellRecord -> String -> Maybe String

such that with

let haskellRecord = HaskellRecord { _key1 = Just "value1"
                                  , _key2 = Just "value2"
                                  , _key3 = Just "value3"
                                  }

then

getKey haskellRecord "value1" == Just "_key1"
getKey haskellRecord "value2" == Just "_key2"
getKey haskellRecord "value3" == Just "_key3"
getKey haskellRecord "value4" == Nothing

Upvotes: 1

Views: 328

Answers (3)

Daniel Wagner
Daniel Wagner

Reputation: 153152

I guess I'd write something like this:

getKey :: String -> HaskellRecord -> [String]
getKey needle haystack =
    [ name
    | (name, selector) <- [("_key1", _key1), ("_key2", _key2), ("_key3", _key3)]
    , selector haystack == Just needle
    ]

See it go:

> getKey "value1" haskellRecord
["_key1"]

...but I suspect this is an X-Y problem. (Because then what are you going to do with that String? If the answer is "turn it back into a selector" or "use it to modify the appropriate field", then why not return a lens or something? And if you actually want a lens, then you probably don't even want this data structure in the first place...)

Upvotes: 7

K. A. Buhr
K. A. Buhr

Reputation: 51099

You can adapt my solution for a similar problem. It uses scrap-your-boilerplate (SYB) generics. That answer explains the code in a fair bit of detail, so I won't explain it here. Below is sample code that works with your example. It supports data types with a mixture of Maybe String and String fields; other field types are ignored by getKey.

Note that this is not going to be very efficient. If this is a core part of your application logic, rather than some debugging hack, you should consider rethinking your data types. Maybe you don't actually want a Haskell record type here, but rather some kind of bi-directional map (e.g., a Map k v and Map v k paired together accessed through a set of functions that keep them consistent).

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

import Data.Tuple
import Data.Generics

data HaskellRecord = HaskellRecord {
  _key1 :: Maybe String
, _key2 :: Maybe String
, _key3 :: Maybe String
} deriving (Data)

-- Get field names (empty list if not record constructor)
getnames :: Data object => object -> [String]
getnames = constrFields . toConstr

-- Get field values as `Maybe String`s
getfields :: Data object => object -> [Maybe String]
getfields = gmapQ toString

-- Generic function to convert one field.
toString :: (Data a) => a -> Maybe String
toString = mkQ    Nothing        -- make a query with default value Nothing
                  id             -- handle:          id     :: Maybe String -> Maybe String
           `extQ` Just           -- extend to:       Just   :: String -> Maybe String

-- Get field name/value pairs from any `Data` object.
getpairs :: Data object => object -> [(String, Maybe String)]
getpairs = zip <$> getnames <*> getfields

getKey :: Data record => String -> record -> Maybe String
getKey v = lookup (Just v) . map swap . getpairs

main :: IO ()
main = do
  print $ getKey "value2" $
    HaskellRecord {
      _key1 = Just "value1",
      _key2 = Just "value2",
      _key3 = Just "value3"
      }

Upvotes: 3

Of course you can write it by hand. It's easy, though slightly tedious and error-prone:

getKey (HaskellRecord (Just x) _ _) y | x == y = Just "_key1"
getKey (HaskellRecord _ (Just x) _) y | x == y = Just "_key2"
getKey (HaskellRecord _ _ (Just x)) y | x == y = Just "_key3"
getKey _ _ = Nothing

It looks like it's possible with Template Haskell or generics too, but there's not some pre-built way to do exactly this, so you'd have to write it yourself, and it'd be (obviously) a bit more complicated. The advantage would be that you'd only have to build it once, and not worry about changing it for data type changes.

Upvotes: 2

Related Questions