2

我正在寻找一个函数,在给定必要的返回类型的情况下,它将完全基于传递给函数的类型的结构返回与该类型匹配的产品参数部分。

例如:

data MyProduct = MyProduct String Int Bool

prod = MyProduct "yes" 0 False

func prod :: Boolean -- would return False
func prod :: String  -- would return "yes"
func prod :: Double  -- compiler error

同样,对于相同的功能func,但不同的产品:

data AnotherProduct = AP (Maybe Int) Char

ap = AP Nothing 'C'

func ap :: Maybe Int -- would return Nothing

有这样的功能吗?我觉得这应该是可能的,也许使用Generic. 我知道这在其他语言中是可能的,例如带有 Shapeless 库的 Scala,但我不知道如何在 Haskell 中最好地解决这个问题。

4

4 回答 4

3

根据@Li-yao_Xia 的回答,可以做到这一点GHC.Generics(这是generic-lens在幕后使用的)。中的代码generic-lens可能有点难以理解,所以这里是你可以从头开始的方法。

工作方式GHC.Generics,它代表一个特定的类型,例如:

data MyProduct = MyProduct String Int Bool deriving (Generic)

通过Rep MyProduct看起来像这样的同构类型:

> :kind! Rep MyProduct
Rep MyProduct :: * -> *
= D1
    ('MetaData "MyProduct" "GenericFetch3" "main" 'False)
    (C1
       ('MetaCons "MyProduct" 'PrefixI 'False)
       (S1
          ('MetaSel
             'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
          (Rec0 String)
        :*: (S1
               ('MetaSel
                  'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
               (Rec0 Int)
             :*: S1
                   ('MetaSel
                      'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
                   (Rec0 Bool))))

诚然,这有点疯狂,但这种嵌套类型的大部分都包含由 、 和 类型表示的元数据D1包装C1S1。如果您删除这些包装器,则归结为:

Rep MyProduct = Rec0 String :*: Rec0 Int :*: Rec0 Bool

这有助于显示表示的结构。

无论如何,要编写一个通用函数,您需要创建一个类型类,它可以处理Rep a使用实例来处理元数据包装器和用于表示产品、总和等的一小组类型构造函数。

在我们的例子中,我们将定义一个类型类Fetch',它允许我们从表示中获取类型的第一个值bt即,so twill beRep MyProduct或类似的东西):

class Fetch' t b where
  fetch' :: t p -> Maybe b

目前,我们不会要求它t实际包含 a b,这就是我们允许fetch'return的原因Nothing

我们需要一个实例来处理元数据:

instance Fetch' t b => Fetch' (M1 i m t) b where
  fetch' (M1 x) = fetch' x

由于所有元数据包装器(D1S1C1)实际上都是别名(M1 DM1 S,分别),我们可以使用通过包装器的实例M1 C来处理它们。M1fetch'

我们还需要一个来处理产品:

instance (Fetch' s b, Fetch' t b) => Fetch' (s :*: t) b where
  fetch' (s :*: t) = fetch' s <|> fetch' t

这只会b从产品的左侧取出,或者 - 失败 - 从右侧取出。

我们需要一个实例来b从匹配类型的(顶级)字段中获取一个(与Rec0上面的匹配,因为这只是 的别名K1 R):

instance Fetch' (K1 i b) b where
  fetch' (K1 x) = Just x

以及一个重叠的包罗万象,将处理错误类型的字段:

instance {-# OVERLAPPABLE #-} Fetch' (K1 i b) a where
  fetch' (K1 _) = Nothing

我们还可以选择在这些表示形式中处理其他可能的类型构造函数(即 、V1U1:+:,我在下面的完整示例中已经完成了这些。

无论如何,有了这些实例,我们可以编写:

fetch1 :: (Generic t, Fetch' (Rep t) b) => t -> b
fetch1 = fromJust . fetch' . from

这很好用:

> fetch1 prod :: String
"yes"
> fetch1 prod :: Int
0
> fetch1 prod :: Bool
False

但与@luqui 基于Data泛型的答案一样,它不会在编译时捕获错误字段,而是在运行时崩溃:

> fetch1 prod :: Double
*** Exception: Maybe.fromJust: Nothing

为了解决这个问题,我们可以引入一个类型族来计算数据结构(或者更确切地说是它Rep)是否实际上包含所需的字段,如下所示:

type family Has t b where
  Has (s :*: t) b = Or (Has s b) (Has t b)
  Has (K1 i b) b = 'True
  Has (K1 i a) b = 'False
  Has (M1 i m t) b = Has t b

使用类型 family 的通常定义Or。现在,我们可以将其作为约束添加到 的定义中fetch

fetch :: ( Generic t
         , Has (Rep t) b ~ 'True
         , Fetch' (Rep t) b)
      => t -> b
fetch = fromJust . fetch' . from

现在我们得到一个错误字段的编译时错误:

> fetch prod :: String
"yes"
> fetch prod :: Double

<interactive>:83:1: error:
    • Couldn't match type ‘'False’ with ‘'True’
        arising from a use of ‘fetch’
    • In the expression: fetch prod :: Double
      In an equation for ‘it’: it = fetch prod :: Double
>

无论如何,将整个事情放在一起,并Has为所有构造函数添加实例和定义,我们得到以下版本。请注意,对于 sum 类型(即(:+:)),它只允许可以在 sum 的所有项中找到的字段类型(因此保证存在)。与 中的typed函数不同generic-lens,此版本允许产品中有多个目标类型的字段,并且只选择第一个。

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleContexts #-}

module GenericFetch where

import Control.Applicative
import Data.Maybe
import GHC.Generics

data MyProduct = MyProduct String Int Bool deriving (Generic)
prod :: MyProduct
prod = MyProduct "yes" 0 False

data AnotherProduct = AP (Maybe Int) Char deriving (Generic)
ap :: AnotherProduct
ap = AP Nothing 'C'

data ASum = A Int String | B Int Double deriving (Generic)
asum :: ASum
asum = A 10 "hello"

class Fetch' t b where
  fetch' :: t p -> Maybe b
instance Fetch' V1 b where
  fetch' _ = Nothing
instance Fetch' U1 b where
  fetch' _ = Nothing
instance (Fetch' s b, Fetch' t b) => Fetch' (s :+: t) b where
  fetch' (L1 s) = fetch' s
  fetch' (R1 t) = fetch' t
instance (Fetch' s b, Fetch' t b) => Fetch' (s :*: t) b where
  fetch' (s :*: t) = fetch' s <|> fetch' t
instance Fetch' (K1 i b) b where
  fetch' (K1 x) = Just x
instance {-# OVERLAPPABLE #-} Fetch' (K1 i b) a where
  fetch' (K1 _) = Nothing
instance Fetch' t b => Fetch' (M1 i m t) b where
  fetch' (M1 x) = fetch' x

type family Has t b where
  Has V1 b = 'False
  Has U1 b = 'False
  Has (s :+: t) b = And (Has s b) (Has t b)
  Has (s :*: t) b = Or (Has s b) (Has t b)
  Has (K1 i b) b = 'True
  Has (K1 i a) b = 'False
  Has (M1 i m t) b = Has t b
type family Or a b where
  Or 'False 'False = 'False
  Or a b = 'True
type family And a b where
  And 'True 'True = 'True
  And a b = 'False

fetch :: ( Generic t
         , Has (Rep t) b ~ 'True
         , Fetch' (Rep t) b)
      => t -> b
fetch = fromJust . fetch' . from

给予:

> :l GenericFetch
> fetch prod :: Int
0
> fetch prod :: Double
...type error...
> fetch ap :: Maybe Int
Nothing
> fetch ap :: Int
...type error...
> fetch asum :: Int
10
> fetch asum :: String
... type error: no string in `B` constructor...
> 
于 2018-08-10T02:39:53.220 回答
2

一种解决方案是generic-lens。特别是,getTyped @T :: P -> T将访问T任何产品类型中的类型字段P(即 的实例Generic)。这是 GHCi 中的一个示例(有关更多详细信息,请参阅自述文件):

> :set -XDeriveGeneric -XTypeApplications
> import Data.Generics.Product
> import GHC.Generics
> data MyProduct = MyProduct String Int Bool deriving Generic
> getTyped @Int (MyProduct "Hello" 33 True)
33
> getTyped @Int (0 :: Int, "hello")
0
于 2018-08-08T11:11:39.283 回答
2

以下是获取所有兼容字段列表的方法:

import Data.Data
import Data.Typeable
import Data.Maybe (maybeToList)

fields :: (Data a, Typeable b) => a -> [b]
fields = gmapQr (++) [] (maybeToList . cast)

您使用的产品类型应该派生Data。这可以自动完成{-# LANGUAGE DeriveDataTypeable #-}

data MyProduct = MyProduct String Int Bool
    deriving (Typeable, Data)

请参阅 和 的gmapQr文档cast

唯一需要注意的是,当您按照您的要求请求不存在的字段时,我想不出一种方法来给出编译时错误。我们需要某种编译时版本的Data.Data. 我不知道有任何这样的事情,尽管我怀疑这是可能的(虽然这可能会更痛苦——这deriving Data为我们做了很多繁重的工作!)。

于 2018-08-07T22:49:07.590 回答
0

这不能在 Haskell 98 标准下完成。一般来说,参数函数不能根据它变成的具体类型来改变行为。它必须保持通用。

作为一个思考过程,为什么可能会考虑这种情况:

data MpProduct a = My Product Int Int String [a]

应该func返回什么要求一个 Int?夏尔什么时候a呢?

现在,我并不是说某个对 GHC 扩展有深入了解的程序员无法做到这一点,但使用标准的 Hindley Milner 类型检查器是不可能的。

于 2018-08-07T22:27:40.723 回答