3
import Data.ConfigFile

data Test = Test 
  { field1 :: Int
  , field2 :: Bool
  , field3 :: String
  } deriving (Show)

whatMyConfigLooksLike = 
    [ ("field1", "5")
    , ("field2", "True")
    , ("field3", "I am a string")
    ]

options = fst . unzip $ whatMyConfigLooksLike

readConfigFile = do
  rv <- runErrorT $ do 
    cp <- join . liftIO $ readfile emptyCP "theconfig.cfg"
    let printn = liftIO . putStrLn
        getn = get x "DEFAULT"
        x = cp
    printn "Loading configuration file..."
    -- I don't want to do the following
    one <- getn "field1"
    two <- getn "field2"
    three <- getn "field3"
    return $ Test one two three -- ...
    -- ... and so on because I have a data type with many fields

    -- I want to fold them onto the data constructor instead
    return $ foldl (\f s -> getn s >>= f) (Test) options
    -- but I think this doesn't type check because f's type is constantly changing?
  print rv

In the above code I have a lambda with a very polymorphic type foldl (\f s -> getn s >>= f). From what I can tell, this causes it to not typecheck in its following recursions.

I think that I can use the RankNTypes language extension for my purpose to define a polymorphic recursive type that can represent any partial application of a function and, hence, allow the function to typecheck. With experimentation, much trial and equal amounts of error, though, I have been unable to come up with anything which compiles.

I would be very grateful if somebody can show me how to implement the RankNTypes extension in terms of the example code above (or suggest alternatives). I'm using GHC 7.4.2.

4

2 回答 2

7

你试图做的事情是不可能的。类型检查器不知道列表中有多少元素,因此不知道您尝试将多少参数传递给构造函数。你这样做了,但这与静态检查的语言无关。

RankNTypes 无济于事,因为根本问题不是您的 lambda 的类型(即使这是类型检查器抛出错误的地方。问题出在您的累加器上:foldl有类型(a -> b -> a) -> a -> [b] -> a;请特别注意,无论有多少扩展你扔给类型检查器,累加器在折叠中的每个点都必须具有相同的类型。Test有类型Int -> Bool -> String -> Test;它的第一个部分应用程序有类型Bool -> String -> Test,并且没有办法统一这些类型。

但是,如果您的程序的其余部分是正确类型的,那么您应该能够简单地使用liftM3 Test (getn "field1") (getn "field2") (getn "field3")作为您的返回,这比您尝试的内容更冗长且更清晰。

于 2013-05-11T02:57:53.943 回答
5

TL;DR :使用“代码摘要”下方列出的模块为您生成代码

你不能忍受编写所有的样板代码。我非常同情。还有其他方法,但我们可以使用 Template Haskell 来生成我们需要的代码。

如果您是 Template Haskell 的新手,您应该查看haskellwiki 页面

首先,让我们打开 Template Haskell 扩展,并导入 Control.Applicative 来整理一下代码:

{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Control.Applicative

我们应该生成什么模板 haskell 代码?

让我们让 ghci 为我们转换一个合适的表达式。getn(为了方便,我伪造了一个函数,所以我可以在独立代码中使用它。)

*Main> :set -XTemplateHaskell
*Main> runQ [| Test <$> getn "field1" <*> getn "field2" <*> getn "field3" |]
InfixE (Just (InfixE (Just (InfixE (Just (ConE Main.Test)) (VarE Data.Functor.<$>) (Just (AppE (VarE Main.getn) (LitE (StringL "field1")))))) (VarE Control.Applicative.<*>) (Just (AppE (VarE Main.getn) (LitE (StringL "field2")))))) (VarE Control.Applicative.<*>) (Just (AppE (VarE Main.getn) (LitE (StringL "field3"))))

哇!让我们整理一下,让它成为有效的haskell代码。首先请注意,诸如此类的表达式Data.Functor.<$>实际上是 type Name。为了得到它,我们可以这样做mkName "<$>",但是字符串修饰是最丑陋的源代码操作,所以让我们'(<$>)改为从函数生成(完全限定的)名称:

whatWeWant = InfixE 
    (Just (InfixE 
             (Just (InfixE 
                      (Just (ConE 'Test)) 
                      (VarE '(<$>)) 
                      (Just (AppE (VarE 'getn) (LitE (StringL "field1")))))) 
             (VarE '(<*>)) 
             (Just (AppE (VarE 'getn) (LitE (StringL "field2")))))) 
    (VarE '(<*>)) 
    (Just (AppE (VarE 'getn) (LitE (StringL "field3"))))

这个(隐藏的)美在于它只是一堆非常相似的表达,我们可以折叠在一起。

生成我们需要的表达式

fieldExpressions :: Name -> [String] -> [Exp]
fieldExpressions getter = map $ \field -> AppE (VarE getter) (LitE (StringL field))

让我们将表达式与 粘合在一起<<*>>用作一种提升:<*><*>

(<<*>>) :: Exp -> Exp -> Exp
a <<*>> b = InfixE  (Just a)  (VarE '(<*>))  (Just b)

现在,当我们获取字段时,我们将首先将构造函数 via<$>应用于第一个字段,然后我们可以将其用作折叠其他字段的基础。

getFields :: Name -> [Exp] -> Exp
getFields _ [] = error "getFields: empty field list"
getFields constructor (f:fs) = foldl (<<*>>) 
                               ( InfixE  (Just $ ConE constructor)  (VarE '(<$>))  (Just f) )
                               fs

快速检查:

*Main> whatWeWant == (getFields 'Test $ fieldExpressions 'getn ["field1","field2","field3"])
True

舞台限制咬

我们可以在同一个源文件中测试/使用它

domything = do
   optionsRecord <- $(return $ getFields 'Test $ fieldExpressions 'getn ["field1","field2","field3"])
   print optionsRecord

除了你会遇到相当不方便的阶段限制:

GHC stage restriction: `getFields'
  is used in a top-level splice or annotation,
  and must be imported, not defined locally

这意味着您必须getFields在另一个模块中定义 etc,然后将其导入到您可以拼接的主文件中。

代码摘要

GetFields.hs:

{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Control.Applicative

module GetFields where

fieldExpressions :: Name -> [String] -> [Exp]
fieldExpressions getter = map $ \field -> AppE (VarE getter) (LitE (StringL field))

(<<*>>) :: Exp -> Exp -> Exp
a <<*>> b = InfixE  (Just a)  (VarE '(<*>))  (Just b)

getFields :: Name -> [Exp] -> Exp
getFields _ [] = error "getFields: empty field list"
getFields constructor (f:fs) = foldl (<<*>>) 
                               ( InfixE  (Just $ ConE constructor)  (VarE '(<$>))  (Just f) )
                               fs

主要.hs:

import GetFields
import Data.ConfigFile

...defs...

readConfigFile = do
  rv <- runErrorT $ do 
    cp <- join . liftIO $ readfile emptyCP "theconfig.cfg"
    let printn = liftIO . putStrLn
        getn = get x "DEFAULT"
        x = cp
    printn "Loading configuration file..."
    someoptions <- $(getFields 'Test $ fieldExpressions 'getn ["field" ++ show n| n<-[1..30]])
于 2013-05-11T18:30:20.960 回答