部分记录的安全替代方案?

时间:2014-05-05 04:58:51

标签: haskell

我正在试图找出一种合理的方法让我的图书馆用户为我提供一系列功能来控制它的行为方式。我想为他们提供一些默认值,他们可以根据自己的需要进行组合和覆盖。显而易见的方式(对我来说)只是Foo {a, b, c, d, e}这样的函数的记录,并使它成为一个monoid,然后提供一些默认值,它们可以mappend在一起。但我制作的默认值不会提供所有功能。因此,我可能会有{a, b}的记录和{c, d}的记录以及{b, c, e}的记录。这显然是不安全的,用户可以为我提供像{a, b, c, e}这样的记录,这会很糟糕。我希望用户能够混合和匹配这样的片段,但仍然必须以完整的记录结束。

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

3 个答案:

答案 0 :(得分:4)

您正在寻找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 }

答案 1 :(得分:0)

您可以使用具有所需选项组合的data类型。并且不要羞于牺牲你的设置monoid的交换性。最后,它就像带有子命令(如git,hg,dnf等)的CLI程序,其选项语法不同。

答案 2 :(得分:0)

您对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