Haskell 具有相同逻辑类型的不同字段的记录

Haskell 具有相同逻辑类型的不同字段的记录,haskell,haskell-lens,Haskell,Haskell Lens,在Haskell程序中,我需要以各种方式从API服务加载记录。有一个loadSmall::IO Small操作,它只加载可用字段中的一些字段。一个loadBig::IO Big操作加载更多字段。也许将来需要更多的“级别”加载 为了简单起见,我们假设Big总是包含Small所做的一切 我希望函数能够以统一的方式访问该类型的这两个“版本”。我读过关于镜头的书,认为我可能会在这里尝试使用它们,但如果有更简单的方法,我根本不打算使用镜头 这就是我想到的: {-# LANGUAGE TemplateHas

在Haskell程序中,我需要以各种方式从API服务加载记录。有一个
loadSmall::IO Small
操作,它只加载可用字段中的一些字段。一个
loadBig::IO Big
操作加载更多字段。也许将来需要更多的“级别”加载

为了简单起见,我们假设
Big
总是包含
Small
所做的一切

我希望函数能够以统一的方式访问该类型的这两个“版本”。我读过关于镜头的书,认为我可能会在这里尝试使用它们,但如果有更简单的方法,我根本不打算使用镜头

这就是我想到的:

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

class HasSmall a where
    name :: Lens' a Text

class HasSmall a => HasBig a where
    email :: Lens' a Text

data Big = Big
    { _bigName :: Text
    , _bigEmail :: Text
    -- ...possibly many more fields
    }
    deriving Show

makeLenses ''Big

instance HasSmall Big where
    name = bigName

instance HasBig Big where
    email = bigEmail

data Small = Small
    { _smallName :: Text
    -- ...probably at least a few fields more
    }
    deriving Show

makeLenses ''Small

instance HasSmall Small where
    name = smallName

-- Function that uses name
useName :: HasSmall a => a -> Text
useName s = "Hello " <> (s ^. name)
{-#语言模板haskell}
进口管制.镜头
a班在哪里
名称::Lens’a文本
类hassall=>HasBig a where
电子邮件::Lens’a文本
数据大=大
{u bigName::Text
,_bigmail::Text
--…可能还有更多的领域
}
衍生节目
使透镜变大
例如:大的在哪里
name=bigName
实例有大有大在哪里
电子邮件=大电子邮件
数据小=小
{u smallName::Text
--…可能至少还有几个领域
}
衍生节目
使透镜变小
实例:小哪里
name=smallName
--使用名称的函数
useName::Hassall a=>a->Text
useName s=“你好”(s^.name)
这看起来像是很多样板,因为现在每个新领域都必须至少在三个地方编写


有没有更有效的方法来实现这一点?

如果
Big
应该包含
Small
中包含的所有内容,那么可以将
Small
设置为
Big
字段:

{-# LANGUAGE RankNTypes #-}
module Main where

class HasSmall a where
    accessSmall :: (Small -> b) -> (a -> b)

data Small = Small
    { name :: String
    , address :: String
    -- ...probably at least a few fields more
    }
    deriving Show

instance HasSmall Small where
    accessSmall = id

data Big = Big
    { small :: Small
    , email :: String
    -- ...possibly many more fields
    }
    deriving Show

instance HasSmall Big where
    accessSmall f = f . small

exampleSmall :: Small
exampleSmall = Small { name = "small name", address = "small address"}

exampleBig :: Big
exampleBig = Big { small = exampleSmall, email = "big email"}

printNameAndAddress :: HasSmall a => a -> IO ()
printNameAndAddress a = do
    putStrLn $ accessSmall name a
    putStrLn $ accessSmall address a

main :: IO ()
main = do
    printNameAndAddress exampleBig
    printNameAndAddress exampleSmall
这种方法不需要镜头,但也可以通过更改
HasSmall
类轻松修改以使用镜头:

class HasSmall a where
    accessSmall :: Lens' Small b -> Lens' a b

instance HasSmall Small where
    accessSmall = id

instance HasSmall Big where
    accessSmall = (.) small
GHCi> :info HasSmall
class HasSmall c where
  small :: Lens' c Small
  name :: Lens' c Text
  {-# MINIMAL small #-}
    -- Defined at Test.hs:16:1
instance HasSmall Small -- Defined at Test.hs:16:1
instance HasSmall Big -- Defined at Test.hs:27:10
GHCi> :set -XTypeApplications
GHCi> :t name @Big
name @Big :: Functor f => (Text -> f Text) -> Big -> f Big
就目前而言,最接近您需要的工具是
makeClassy

data Small = Small
    { _name :: Text
    -- ...probably at least a few fields more
    }
    deriving Show

makeClassy ''Small

data Big = Big
    { _bigSmall :: Small
    , _bigEmail :: Text
    -- ...possibly many more fields
    }
    deriving Show

makeClassy ''Big  -- As far as this demo goes, not really necessary.

instance HasSmall Big where
    small = bigSmall
这种方法要求您在
Big
中有一个
Small
字段,以便可以通过生成的
hasmall
类来访问
Small
中的字段:

class HasSmall a where
    accessSmall :: Lens' Small b -> Lens' a b

instance HasSmall Small where
    accessSmall = id

instance HasSmall Big where
    accessSmall = (.) small
GHCi> :info HasSmall
class HasSmall c where
  small :: Lens' c Small
  name :: Lens' c Text
  {-# MINIMAL small #-}
    -- Defined at Test.hs:16:1
instance HasSmall Small -- Defined at Test.hs:16:1
instance HasSmall Big -- Defined at Test.hs:27:10
GHCi> :set -XTypeApplications
GHCi> :t name @Big
name @Big :: Functor f => (Text -> f Text) -> Big -> f Big
另一种方法是通过
makeFields
对字段进行抽象:

data Small = Small
    { _smallName :: Text
    -- ...probably at least a few fields more
    }
    deriving Show

makeFields ''Small

data Big = Big
    { _bigName :: Text
    , _bigEmail :: Text
    -- ...possibly many more fields
    }
    deriving Show

makeFields ''Big

在这个用例中,
makeFields
的一个潜在缺点是,正如您所注意到的,机器完全打开,可以为字段指定哪些类型。(相比之下,
makeClassy
示例中的
Small
的定义间接指定了任何
名称
镜头都将具有
文本
类型的目标。)

镜头的第四个机制包括,它生成以不同方向抽象的类(即,每个字段一个类)。如果您只需要在表示中统一访问字段,那么这可能就足够了。谢谢,这看起来确实是一个不错的选择。我有点惊讶makeFields似乎没有验证字段是否具有相同的类型。我可以将bigName更改为Int,程序仍然可以编译。如果HasName可以将名称的类型固定为Text,那么可能会稍微好一些,这样所有消费函数的类型签名就不需要指出name::Text。我没有立即在镜头文档中看到这样的选项。