【问题标题】:Deriving projection functions using `generics-sop`使用 `generics-sop` 导出投影函数
【发布时间】:2021-04-07 18:41:20
【问题描述】:

我将如何导出函数

getField :: (Generic a, HasDatatypeInfo a) => Proxy (name :: Symbol) -> a -> b

使用类型级字符串 (Symbol) 从任意记录投影字段,使用 generics-sop 库?

这个和Retrieving record function in generic SOP类似,但是我有以下问题:

  • OP 没有解释如何走最后一英里来获得我想要的签名。
  • OP 定义了复杂的特殊用途帮助器类型,我很想避免这种情况
  • 给定的解决方案仅在运行时出错,但编译时匹配应该是可能的,因为类型级别的 DataTypeInfo 是通过 DatatypeInfoOf 类型族提供的(很高兴拥有,但不是必需的)。李>

lens-sop 包也是 seems to do something similar,但我不知道如何让它适合我。

我也更喜欢使用 IsProductType 类型类的解决方案。

【问题讨论】:

  • records-sop package 具有大部分功能,尽管我认为它实际上并没有导出所需的提取功能。 (哦,我看到你在此期间做了公关。)
  • 令人讨厌的是,records-sop 与其他 generics-sop 习语不太兼容,例如 IsProoductType 和标准元数据,我非常依赖这些。
  • 我不知道你为什么对IsProductType 如此感兴趣。它只是一些不足以完成此任务的约束的类型同义词。

标签: haskell metaprogramming ghc type-level-computation ghc-generics


【解决方案1】:

我知道这是一个乱七八糟的答案,并不是您真正想要的,但这是我现在能做的最好的。请注意,这适用于产品类型和总和类型,其中 所有 构造函数具有指定的字段名称。

我认为通过将名称查找与产品处理的其余部分分开,这可能会有所简化。即:使用数据类型信息来计算字段编号(作为一元自然数),然后使用该数字来挖掘代码。不幸的是,generics-sop 似乎没有非常好的工具来处理列表压缩,所以我最终做了很多“手工”。

{-# language EmptyCase, GADTs, TypeFamilies, DataKinds, TypeOperators, RankNTypes #-}
{-# language UndecidableInstances, UndecidableSuperClasses #-}
{-# language AllowAmbiguousTypes, TypeApplications, MultiParamTypeClasses,
  FlexibleContexts, FlexibleInstances, MagicHash, UnboxedTuples, ScopedTypeVariables #-}
{-# language ConstraintKinds #-}
{-# OPTIONS_GHC -Wall #-}

module Data.Proj where
import Data.Kind (Type, Constraint)
import Generics.SOP
import Generics.SOP.Type.Metadata as GST
import GHC.TypeLits
import Data.Type.Equality (type (==))

-- This is what you were looking for, but slightly more flexible.
genericPrj :: forall s b a.
  ( Generic a
  , HasFieldNS s b (GetConstructorInfos (DatatypeInfoOf a)) (Code a))
  => a -> b
genericPrj a = case genericPrj# @s a of (# b #) -> b

-- This version lets you force the *extraction* of a field without
-- forcing the field itself.
genericPrj# :: forall s b a.
  ( Generic a
  , HasFieldNS s b (GetConstructorInfos (DatatypeInfoOf a)) (Code a))
  => a -> (# b #)
genericPrj# a = case from a of
  SOP xs -> extraction @s @b @(GetConstructorInfos (DatatypeInfoOf a)) @(Code a) xs

-- | Extract info about the constructor(s) from 'GST.DatatypeInfo'.
type family GetConstructorInfos (inf :: GST.DatatypeInfo) :: [GST.ConstructorInfo] where
  GetConstructorInfos ('GST.ADT _ _ infos _) = infos
  GetConstructorInfos ('GST.Newtype _ _ info) = '[info]

class HasFieldNS (s :: Symbol) b (cis :: [GST.ConstructorInfo]) (code :: [[Type]]) where
  extraction :: NS (NP I) code -> (# b #)
instance HasFieldNS s b cis '[] where
  extraction x = case x of
instance (HasFieldNP' s b r c, HasFieldNS s b cis cs, rec ~ 'GST.Record q r, VerifyRecord rec)
    => HasFieldNS s b (rec ': cis) (c ': cs) where
  extraction (Z x) = extractIt @s @b @rec @c x
  extraction (S x) = extraction @s @b @cis @cs x

type family VerifyRecord rec :: Constraint where
  VerifyRecord ('GST.Record _ _) = ()
  VerifyRecord _ = TypeError ('Text "Constructor is not in record form.")

-- | Given info about a constructor, a list of its field types, and the name and
-- type of a field, produce an extraction function.
class HasFieldNP (s :: Symbol) b (ci :: GST.ConstructorInfo) (fields :: [Type]) where
  extractIt :: NP I fields -> (# b #)
instance (HasFieldNP' s b fi fields, ci ~ 'GST.Record _cn fi)
    => HasFieldNP s b ci fields where
  extractIt = extractIt' @s @_ @fi

class HasFieldNP' (s :: Symbol) b (fi :: [GST.FieldInfo]) (fields :: [Type]) where
  extractIt' :: NP I fields -> (# b #)

class TypeError ('Text "Can't find field " ':<>: 'ShowType s)
    => MissingField (s :: Symbol) where
  impossible :: a

instance MissingField s => HasFieldNP' s b fi '[] where
  extractIt' = impossible @s ()

instance HasFieldNP'' s b (fi == s) field fis fields =>
  HasFieldNP' s b ('GST.FieldInfo fi ': fis) (field ': fields) where
  extractIt' = extractIt'' @s @b @(fi == s) @field @fis @fields

class HasFieldNP'' (s :: Symbol) b (match :: Bool) (field :: Type) (fis :: [GST.FieldInfo]) (fields :: [Type]) where
  extractIt'' :: NP I (field ': fields) -> (# b #)
instance b ~ field => HasFieldNP'' _s b 'True field fis fields where
  extractIt'' (I x :* _) = (# x #)
instance (HasFieldNP' s b fis fields) => HasFieldNP'' s b 'False _field fis fields where
  extractIt'' (_ :* fields) = extractIt' @s @b @fis fields

示例

data Foo
  = Foo {blob :: Int, greg :: String}
  | Bar {hello :: Char, blob :: Int}
deriveGeneric ''Foo

genericPrj @"blob" (Foo 12 "yo") ===> 12
genericPrj @"blob" (Bar 'x' 5) ===> 5
genericPrj# @"blob" (Bar 'x' 5) ===> (# 5 #)

myAbsurd :: Void -> a
myAbsurd = genericPrj @"whatever"

data Booby a
  = Booby {foo :: a}
  | Bobby {bar :: a}
deriveGeneric ''Booby

genericPrj @"foo" (Booby 'a')
-- Type error because Bobby has no foo field

【讨论】:

  • 我真的很想完全避免TemplateHaskell
  • @AriFordsham,我只用它来导出示例的Generic 实例。您可以改为为每个派生 GHC.Generic 并给实例(或派生 anyclassGenericHasDatatypeInfo
  • 必须有更简单的方法。 records-sop 有一个私有函数,几乎可以做到这一点。我必须在全部重写、等待我的拉取请求通过 Hackage 或构建自定义包之间做出选择。 github.com/kosmikus/records-sop/pull/4
  • @AriFordsham,是的,我可以将其剥离,使其仅适用于单构造函数类型,但我更感兴趣的是重写它,以便获得镜头和遍历。
【解决方案2】:

从 0.1.1.0 版开始,records-sop 提供 this function

getField :: forall s a b ra. (IsRecord a ra, IsElemOf s b ra) => a -> b

它需要作为类型应用程序而不是代理提供的字段名称,如下所示:

data Foo = Foo { bar :: Int }

getField @"bar" (Foo 42) === 42

这提供了编译时提取,尽管它仍然需要一些转换以适应我的项目中操作标准 generics-sop 元数据的现有代码。

这仅适用于单构造函数类型。 @dfeuer 的回答也支持 sum 类型。

感谢@kosmikus,generics-sop 的合著者和records-sop 的作者,为回答这个问题而实现了这一点!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-03
    • 2013-07-19
    • 1970-01-01
    相关资源
    最近更新 更多