【问题标题】:Safe alternative to partial records?部分记录的安全替代方案?
【发布时间】:2014-05-05 04:58:50
【问题描述】:

我正在尝试找出一种合理的方法,让我的库的用户为我提供一堆函数来控制它的行为方式。我想为他们提供一些默认值,他们可以在他们认为合适的时候组合和覆盖。显而易见的方法(对我来说)只是记录Foo {a, b, c, d, e} 之类的函数,并将其设为一个幺半群,然后提供一些默认值,它们可以一起mappend。但我所做的默认设置不会提供所有功能。所以我可能有{a, b} 的记录和{c, d} 的记录和{b, c, e} 的另一个记录。这显然不安全,用户可以向我提供像{a, b, c, e} 这样的记录,这很糟糕。我希望用户能够混合和匹配这样的片段,但最终还是要得到完整的记录。

有没有一种安全的方法来做这样的事情?如果我将记录中的所有函数都变成了 Maybe 函数,那么我至少会这样做,这样我就可以检查提供的值是否缺少函数,但是他们仍然在运行时而不是编译时收到该错误。如果可以的话,我宁愿让编译器强制执行“必须提供记录中的所有字段”不变量。

【问题讨论】:

  • 如果你真的需要部分默认值,我认为没有一个整洁的解决方案。如果你想要编译时安全,你需要将区别带入类型级别,通过(按照混乱的递增顺序)具有不同参数组合的 Foo 构建函数,对应的 PreFoo sum 类型,每个部分默认设置一个构造函数,或者多种不同的 pre-Foo 类型服务于相同的目的。

标签: haskell


【解决方案1】:

您正在寻找data-default 包。使用它,您可以安全地为您的类型初始化默认值。示例:

import Data.Default

data Foo = Foo { a :: Int, b :: Int }

instance Default Foo where
  def = Foo 3 3

现在使用def,您可以在任何您需要的函数中使用默认值:

dummyFun :: Foo -> Foo
dummyFun x = def

您也可以根据自己的需要更改记录值:

dummyFun :: Foo -> Foo
dummyFun x = def { b = 8 }

【讨论】:

  • 我无法为它们填写单数默认值。这就是为什么他们需要提供所有功能。
  • @user3261399 提供任意默认值是否有意义?另外,我可能过于从字面上理解您的示例,但如果您可以在不同的情况下提供 {a, b}{c, d}{a, b, c, e} 作为部分默认值,那么您似乎对每个字段都有一个合理的默认值。
【解决方案2】:

您对Monoid 的想法是一个好的开始,但Monoid 还不够笼统。你真正需要的是Category。您可以为工作定制一个,但您甚至可以通过放弃使用单个固定类型来表示部分记录的想法,而是让存在不同字段的部分记录具有不同类型,从而避免考虑类别。这意味着您正在使用类型和函数的类别,有时称为 Hask,但您不必考虑这一点。警告:Hackage 上的一个花哨的记录包可能使这类事情更容易做,但我(还)没有充分理解它们中的任何一个来使用它们,更不用说推荐它们了。

首先是样板

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}

module PartialRec where
import Data.Proxy

现在是类型。 PList 表示(部分)记录。它的第一个参数列表表示记录字段的类型,而第二个参数列表表示存在哪些字段。

-- Skip is totally unnecessary, but makes the
-- syntax of skips a bit less horrible.
data Skip = Skip
infixr 6 `PCons`, `PSkip`
data PList :: [*] -> [Bool] -> * where
  PNil :: PList '[] '[]
  PCons :: a -> PList as bs -> PList (a ': as) ('True ': bs)
  PSkip :: Skip -> PList as bs -> PList (a ': as) ('False ': bs)

我们使用类型族来表达当两个部分记录组合时类型是如何组合的。特别是,任何部分记录中存在的任何字段都将出现在结果中。

type family Combine (as :: [Bool]) (bs :: [Bool]) :: [Bool] where
  Combine '[] '[] = '[]
  Combine ('True ': xs) (y ': ys) = 'True ': Combine xs ys
  Combine ('False ': xs) (y ': ys) = y ': Combine xs ys

combine 函数将两个部分记录组合成一个具有相同字段类型和任一部分记录中存在的任何字段的新记录。如果两个记录中都存在一个字段,则选择第一个。

combine :: PList as bs -> PList as cs -> PList as (Combine bs cs)
combine PNil PNil = PNil
combine (PCons x xs) (PSkip _ ys) = PCons x (combine xs ys)
combine (PSkip _ xs) (PCons y ys) = PCons y (combine xs ys)
combine (PSkip _ xs) (PSkip _ ys) = PSkip Skip (combine xs ys)
combine (PCons x xs) (PCons _ ys) = PCons x (combine xs ys)

默认逻辑留给buildRecbuildRec 获取具有足够字段集的部分记录,并根据必填字段和实际存在的任何可选字段为可选字段生成值。 buildRec 实际上是使用类型类实现的,其实例由类型族选择,以支持多个足够的字段集。

-- Names for instances
data BuilderTag = Builder1 | Builder2

-- Given a list of types present, determines
-- the correct Builder instance to use.
type family ChooseBuilder (present :: [Bool]) :: BuilderTag where
  ChooseBuilder '[ 'True, 'True, 'True, b3 ] = Builder2
  ChooseBuilder '[ 'True, b1, 'True, b2 ] = Builder1

class Builder (tag :: BuilderTag) (present :: [Bool]) where
  buildRec' :: proxy tag -> PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer)

buildRec :: forall tag present . (Builder tag present, tag ~ ChooseBuilder present)
         => PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer)
buildRec xs = buildRec' (Proxy :: Proxy tag) xs

instance Builder 'Builder1 '[ 'True, b1, 'True, b2 ] where
  buildRec' _ (i `PCons` Skip `PSkip` b `PCons` Skip `PSkip` PNil) = (i, toEnum (i + fromEnum b) , b, if i > 3 && b then 12 else 13)
  buildRec' _ (i `PCons` Skip `PSkip` b `PCons` intg `PCons` PNil) = (i, toEnum (i + fromEnum b + fromIntegral intg), b, intg)
  buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i)
  buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg)

instance Builder 'Builder2 '[ 'True, 'True, 'True, b3 ] where
  buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i)
  buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg)

这里有一些函数可以构建部分记录,每个记录都有一个字段。

justInt :: Int -> PList '[Int, a, b, c] '[ 'True, 'False, 'False, 'False]
justInt x = x `PCons` Skip `PSkip` Skip `PSkip` Skip `PSkip` PNil

justChar :: Char -> PList '[a, Char, b, c] '[ 'False, 'True, 'False, 'False]
justChar x = Skip `PSkip` x `PCons` Skip `PSkip` Skip `PSkip` PNil

justBool :: Bool -> PList '[a, b, Bool, c] '[ 'False, 'False, 'True, 'False]
justBool x = Skip `PSkip` Skip `PSkip` x `PCons` Skip `PSkip` PNil

justInteger :: Integer -> PList '[a, b, c, Integer] '[ 'False, 'False, 'False, 'True]
justInteger x = Skip `PSkip` Skip `PSkip` Skip `PSkip` x `PCons` PNil

以下是一些示例用途。 useChar 最终将使用 Builder2 实例,而 noChar 将使用 Builder1 实例。

useChar :: (Int, Char, Bool, Integer)
useChar = buildRec $ justInt 12 `combine` justBool False `combine` justChar 'c'

noChar :: (Int, Char, Bool, Integer)
noChar = buildRec $ justInt 12 `combine` justBool False

【讨论】:

  • 你的“组合”在Bracha and Lindstrom 中被称为“记录覆盖”你有没有发现在Haskell 中是否有不那么可怕的方法来做到这一点?我看到有一个博客/wiki 页面...gitlab.haskell.org/ghc/ghc/-/wikis/records
  • @Fizz,我写这篇文章已经快四年了,我想我从那时起就没有考虑过。可能有更好的方法!现在我的 Haskell 游戏时间专注于 <*Data.Sequence;我真的很想把它放在床上......
【解决方案3】:

您可以使用带有您想要的选项组合的data 类型。并且不要害羞牺牲设置幺半群的交换性。最后,它就像带有选项语法不同的子命令(如 git、hg、dnf 等)的 CLI 程序。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-15
    • 1970-01-01
    • 2017-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多