1

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
George
  • 6,927
  • 4
  • 34
  • 67
  • So you want to return the key, not the value? – Willem Van Onsem May 05 '20 at 19:34
  • 1
    Are you asking if it's possible to write by hand, or are you asking if there's some pre-existing machinery that uses generics or Template Haskell or something to write it for you? – Joseph Sible-Reinstate Monica May 05 '20 at 19:39
  • @WillemVanOnsem: Yes, basically "search by value, for keys". – George May 05 '20 at 19:45
  • 1
    @JosephSible-ReinstateMonica It'd be nice to avoid Template Haskell? But if that's required then yes. – George May 05 '20 at 19:46
  • What should `getKey (HaskellRecord { _key1 = Just "foo", _key2 = Just "foo", _key3 = Just "foo" }) "foo"` return? – Benjamin Hodgson May 05 '20 at 19:48
  • @BenjaminHodgson Good question. I suppose the first key it finds is reasonable, so in this case `Just "_key1"`. – George May 05 '20 at 19:50
  • 1
    How should `getKey` behave when the record contains fields of different types? – Benjamin Hodgson May 05 '20 at 20:08
  • 5
    Why do you want to do this? What are you going to do with the `String` once you have it? I suspect there's a better design for whatever top-level effect you're trying to make happen. – Daniel Wagner May 05 '20 at 20:24
  • I agree with Daniel. What I was driving at with my earlier questions is that this function is hard to specify. The reason this function is hard to specify is because it's not how records are meant to be used really – Benjamin Hodgson May 05 '20 at 20:30
  • Are you really asking for a value to key mapping..? Accessors (keys) are unique and values are not... So what if there are multiple keys with the same string..? Which key do you expect to have..? You shouldn't need this in your logic. – Redu May 05 '20 at 21:55
  • It seems it's possible [to get a list of accessors of a record](https://stackoverflow.com/a/8458117/4543207) like how it is done with `Object.keys()` in JS but in a strongly typed language like Haskell i can not imageine a case why you need to do that since you are already sure about the types and know the accessors in advance. – Redu May 05 '20 at 22:25

3 Answers3

7

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...)

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
3

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"
      }
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
2

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.