Haskell динамически устанавливает поле записи на основе строки имени поля?

Скажем, у меня есть следующая запись:

data Rec = Rec {
   field1 :: Int,
   field2 :: Int
}

Как мне написать функцию:

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value 

так что я могу передать строки «field1» или «field2» в аргумент fieldName и обновить связанное поле? Я понимаю, что здесь использовать Data.Data и Data.Typeable, но я не могу понять эти два пакета.


Пример библиотеки, которую я видел, это cmdArgs. Ниже приводится выдержка из записи в блоге о том, как использовать эту библиотеку. :

{-# LANGUAGE DeriveDataTypeable #-}
import System.Console.CmdArgs

data Guess = Guess {min :: Int, max :: Int, limit :: Maybe Int} deriving (Data,Typeable,Show)

main = do
    x <- cmdArgs $ Guess 1 100 Nothing
    print x

Теперь у нас есть простой парсер командной строки. Вот некоторые примеры взаимодействий:

$ guess --min=10
NumberGuess {min = 10, max = 100, limit = Nothing}

person Ana    schedule 28.12.2011    source источник
comment
Вы, вероятно, не захотите этого делать. Вы слышали о линзах? Я думаю, что единственный способ добиться этого - это взломать пары имен полей с их индексами аргументов и использовать gmapQi или что-то подобное. (Вам нужно добавить deriving (Typeable, Data) к объявлению записи, чтобы это имело хоть какую-то надежду на работу; это невозможно сделать для произвольных типов.)   -  person ehird    schedule 28.12.2011
comment
Я действительно хочу это сделать. Я хотел бы создать библиотеку, в которой пользователь может предоставить запись, а библиотека может заполнить запись, проанализировав некоторый текст. Текст будет содержать ссылки на поле в записи, которое я хочу установить.   -  person Ana    schedule 28.12.2011
comment
Лучше избегать привязки реализации этой пользовательской функциональности к деталям внутренней реализации имен полей записи. Второй вариант - решение на основе линз, предложенное @pat; вы можете автоматизировать создание recMap из имен полей записи с помощью Template Haskell.   -  person ehird    schedule 28.12.2011
comment
ehird, пожалуйста, посмотрите мой обновленный вопрос. cmdArgs - это пример библиотеки, которая делает то, что я ищу. Я не вижу в этом проблемы.   -  person Ana    schedule 28.12.2011
comment
Боюсь, плохой выбор примера; Неявный режим cmdargs - одна из самых ругаемых библиотек Haskell за его нечистоту :) Тем не менее, если вы действительно хотите этого добиться, я все же предлагаю использовать Template Haskell для генерации recMap; он более гибкий и менее волшебный.   -  person ehird    schedule 28.12.2011
comment
Это идет вразрез с менталитетом Haskell о безопасности времени компиляции. Вам следует подумать об использовании менее динамичного решения   -  person hugomg    schedule 28.12.2011
comment
Понятно. cmdArgs делает то, что делает, потому что он нечист? У меня создалось впечатление, что он делает то, что делает, используя Typeable и Data.   -  person Ana    schedule 28.12.2011
comment
@Ana: Нет, он, вероятно, использует экземпляр Data для достижения этой конкретной функциональности, но в целом он основан на полностью нечистом интерфейсе, поэтому это не очень хороший стандарт того, что должен делать хороший код Haskell :)   -  person ehird    schedule 28.12.2011
comment
Тогда я все еще не понимаю, в чем проблема. Можно использовать Typeable и Data для того, что я ищу, которые являются стандартными пакетами, и не нужно использовать сторонний пакет или использовать Template Haskell. Конечно, это звучит так, как будто статическая типизация обходится стороной, но я хотел бы узнать, как это можно сделать, чтобы у меня была возможность сделать осознанный выбор, а не слепо выбирать популярный вариант, потому что другой не является предпочтительным. .   -  person Ana    schedule 29.12.2011
comment
@Ana: Конечно, но полный ответ на подобный вопрос может повлечь за собой указание на то, что, вероятно, существует гораздо лучший способ достижения той же цели; если бы кто-то спросил, как я могу использовать unsafeCoerce для преобразования между целочисленными типами в Haskell ?, было бы упущением не указать, что вместо этого вы должны использовать fromIntegral; таким образом мои комментарии.   -  person ehird    schedule 29.12.2011


Ответы (2)


Хорошо, вот решение, которое не использует шаблон haskell или требует, чтобы вы вручную управляли картой поля.

Я реализовал более общий modifyField, который принимает функцию мутатора, и реализовал setField (в девичестве changeField), используя его с const value.

Сигнатура modifyField и setField является общей как для записи, так и для типа мутатора / значения; однако, чтобы избежать Num двусмысленности, числовые константы в примере вызова должны иметь явные :: Int подписи.

Я также изменил порядок параметров, так что rec идет последним, что позволило создать цепочку из _10 _ / _ 11_ при нормальной композиции функций (см. Последний пример вызова).

modifyField построен на основе примитива gmapTi, который является "отсутствующей" функцией из Data.Data. Это нечто среднее между gmapT и gmapQi.

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RankNTypes #-}

import Data.Typeable (Typeable, typeOf)
import Data.Data (Data, gfoldl, gmapQi, ConstrRep(AlgConstr),
                  toConstr, constrRep, constrFields)
import Data.Generics (extT, extQ)
import Data.List (elemIndex)
import Control.Arrow ((&&&))

data Rec = Rec {
    field1 :: Int,
    field2 :: String
} deriving(Show, Data, Typeable)

main = do
  let r = Rec { field1 = 1, field2 = "hello" }
  print r
  let r' = setField "field1" (10 :: Int) r
  print r'
  let r'' = setField "field2" "world" r'
  print r''
  print . modifyField "field1" (succ :: Int -> Int) . setField "field2" "there" $ r
  print (getField "field2" r' :: String)

---------------------------------------------------------------------------------------

data Ti a = Ti Int a

gmapTi :: Data a => Int -> (forall b. Data b => b -> b) -> a -> a
gmapTi i f x = case gfoldl k z x of { Ti _ a -> a }
  where
    k :: Data d => Ti (d->b) -> d -> Ti b
    k (Ti i' c) a = Ti (i'+1) (if i==i' then c (f a) else c a)
    z :: g -> Ti g
    z = Ti 0

---------------------------------------------------------------------------------------

fieldNames :: (Data r) => r -> [String]
fieldNames rec =
  case (constrRep &&& constrFields) $ toConstr rec of
    (AlgConstr _, fs) | not $ null fs -> fs
    otherwise                         -> error "Not a record type"

fieldIndex :: (Data r) => String -> r -> Int
fieldIndex fieldName rec =
  case fieldName `elemIndex` fieldNames rec of
    Just i  -> i
    Nothing -> error $ "No such field: " ++ fieldName

modifyField :: (Data r, Typeable v) => String -> (v -> v) -> r -> r
modifyField fieldName m rec = gmapTi i (e `extT` m) rec
  where
    i = fieldName `fieldIndex` rec
    e x = error $ "Type mismatch: " ++ fieldName ++
                             " :: " ++ (show . typeOf $ x) ++
                           ", not " ++ (show . typeOf $ m undefined)

setField :: (Data r, Typeable v) => String -> v -> r -> r
setField fieldName value = modifyField fieldName (const value)

getField :: (Data r, Typeable v) => String -> r -> v
getField fieldName rec = gmapQi i (e `extQ` id) rec
  where
    i = fieldName `fieldIndex` rec
    e x = error $ "Type mismatch: " ++ fieldName ++
                             " :: " ++ (show . typeOf $ x) ++
                           ", not " ++ (show . typeOf $ e undefined)
person Community    schedule 28.12.2011

Вы можете построить карту от названий полей до их линз:

{-# LANGUAGE TemplateHaskell #-}
import Data.Lens
import Data.Lens.Template
import qualified Data.Map as Map

data Rec = Rec {
    _field1 :: Int,
    _field2 :: Int
} deriving(Show)

$( makeLens ''Rec )

recMap = Map.fromList [ ("field1", field1)
                      , ("field2", field2)
                      ]

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value = set rec
    where set = (recMap Map.! fieldName) ^= value

main = do
  let r = Rec { _field1 = 1, _field2 = 2 }
  print r
  let r' = changeField r "field1" 10
  let r'' = changeField r' "field2" 20
  print r''

или без линз:

import qualified Data.Map as Map

data Rec = Rec {
    field1 :: Int,
    field2 :: Int
} deriving(Show)

recMap = Map.fromList [ ("field1", \r v -> r { field1 = v })
                      , ("field2", \r v -> r { field2 = v })
                      ]

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value =
    (recMap Map.! fieldName) rec value

main = do
  let r = Rec { field1 = 1, field2 = 2 }
  print r
  let r' = changeField r "field1" 10
  let r'' = changeField r' "field2" 20
  print r''
person pat    schedule 28.12.2011
comment
RecMap - это именно тот элемент, которого я избегаю. Я требую, чтобы я специализировался для каждого поля, и я хотел бы выполнять отображение строки в поле динамически. - person Ana; 28.12.2011