我发现自己在设计中遇到了相同的模式,我从一个带有几个数据构造函数的类型开始,最终希望能够针对那些数据构造函数进行类型化,然后将它们拆分为自己的类型,直到那时必须在我仍然需要表示其中多种类型(即集合)的情况下,通过使用Either或另一个标记联合来增加程序其他部分的冗长性。
我希望有人可以指出我要完成我尝试做的更好的方法。让我从一个简单的例子开始。我正在对一个测试系统进行建模,在这里您可以拥有嵌套的测试套件,这些套件最终会在测试中结束。所以,像这样:
data Node =
Test { source::string }
Suite { title::string, children::[Node] }
因此,到目前为止,还很简单,基本上是一个精美的Tree / Leaf声明。但是,我很快意识到我希望能够创建专门用于测试的功能。这样,我现在将其拆分为:
data Test = Test { source::string }
data Suite = Suite { title::string, children::[Either Test Suite] }
或者,我可以选择“自定义”(特别是如果该示例更复杂并且具有两个以上选项),请输入以下内容:
data Node =
fromTest Test
fromSuite Suite
因此,已经很不幸的是,仅仅能够拥有一个可以同时包含套件或测试的Suite
类,我最终得到了一个怪异的开销Either
类(无论它是否包含一个实际的Either
或自定义的)。如果使用存在性类型类,则可以使Test
和Suite
都派生“ Node_”,然后让Suite
拥有一个上述Node
的列表,这使我无所适从。副产品将允许类似的操作,在这种情况下,我基本上会执行相同的Either
策略,而不会引起标签的冗长。
现在让我通过一个更复杂的示例进行扩展。可以跳过测试(禁用测试),成功,失败或忽略测试的结果(由于先前的失败而无法运行测试或套件)。再说一次,我最初是这样的:
data Result = Success | Omitted | Failure | Skipped
data ResultTree =
Tree { children::[ResultTree], result::Result } |
Leaf Result
但是我很快意识到我希望能够编写具有特定结果的函数,更重要的是,让类型本身强制所有权属性:成功的套件必须仅拥有Success或Skipped子代,Failure的子代可以是任何东西,被忽略的只能拥有被忽略的,依此类推。所以现在我得到这样的结果:
data Success = Success { children::[Either Success Skipped] }
data Failure = Failure { children::[AnyResult] }
data Omitted = Omitted { children::[Omitted] }
data Skipped = Skipped { children::[Skipped] }
data AnyResult =
fromSuccess Success |
fromFailure Failure |
fromOmitted Omitted |
fromSkipped Skipped
同样,我现在拥有AnyResult
之类的这些奇怪的“包装器”类型,但是,我得到了以前只能从运行时操作强制执行的某种类型的强制执行。是否有一个更好的策略而不涉及打开诸如生存类型类之类的功能?
答案 0 :(得分:3)
读到你的句子时,我想到的第一件事是:精炼类型。
它们只允许从类型中获取一些值作为输入,并使这些约束进行编译时检查/错误。
这段视频来自HaskellX 2018上的一次演讲,介绍了LiquidHaskell,它允许在Haskell中使用优化类型:
您必须修饰haskell函数签名,并安装LiquidHaskell:
f :: Int -> i : Int {i | i < 3} -> Int
是一个只能接受在编译时检查的值为Int
的{{1}}作为第二个参数的函数。
您最好对< 3
类型设置约束。
答案 1 :(得分:2)
我认为您可能正在寻找GADTs
和DataKinds
。这使您可以将数据类型中每个构造函数的类型优化为一组特定的可能值。例如:
data TestType = Test | Suite
data Node (t :: TestType) where
TestNode :: { source :: String } -> Node 'Test
SuiteNode :: { title :: String, children :: [SomeNode] } -> Node 'Suite
data SomeNode where
SomeNode :: Node t -> SomeNode
然后,当一个函数仅在测试中运行时,它可以使用Node 'Test
;在套件中,Node 'Suite
;以及多态的Node a
上。在Node a
上进行模式匹配时,每个case
分支都可以访问相等约束:
useNode :: Node a -> Foo
useNode node = case node of
TestNode source -> {- here it’s known that (a ~ 'Test) -}
SuiteNode title children -> {- here, (a ~ 'Suite) -}
实际上,如果您使用了具体的Node 'Test
,编译器将禁止SuiteNode
分支,因为它永远无法匹配。
SomeNode
是用于包装未知类型的Node
的共存物。您可以根据需要为此添加额外的类约束。
您可以使用Result
做类似的事情:
data ResultType = Success | Omitted | Failure | Skipped
data Result (t :: ResultType) where
SuccessResult
:: [Either (Result 'Success) (Result 'Skipped)]
-> Result 'Success
FailureResult
:: [SomeResult]
-> Result 'Failure
OmittedResult
:: [Result 'Omitted]
-> Result 'Omitted
SkippedResult
:: [Result 'Skipped]
-> Result 'Skipped
data SomeResult where
SomeResult :: Result t -> SomeResult
我当然会在您的实际代码中假设有这些类型的更多信息;实际上,它们代表的并不多。如果您进行的动态计算(例如运行测试)可能会产生不同类型的结果,则可以将其包装在SomeResult
中返回。
为了处理动态结果,您可能需要向编译器证明两种类型是相等的;为此,我将指导您转到Data.Type.Equality
,它提供了一种类型为a :~: b
的类型Refl
,当两种类型a
和b
为等于;您可以对此进行模式匹配,以通知类型检查器有关类型相等性的信息,或使用各种组合器进行更复杂的证明。
GADTs
也可以与ExistentialTypes
(和RankNTypes
,一般来说是consumeResult :: SomeResult -> (forall t. Result t -> r) -> r
consumeResult (SomeResult res) k = k res
)一起使用,它基本上使您可以将多态函数作为参数传递给其他函数;如果要通用地使用一个存在项,则这是必需的:
k
这是 continuation-passing style (CPS)的示例,其中create table #TempTable (InvoiceNum int,State varchar(2), ChargeName varchar(50), PercentageRate decimal(5,3), FlatRate decimal(5,2))
insert into #TempTable values (235736, 'AZ','Inspection & Policy Fee', NULL,250.00)
,(235736, 'AZ','Surplus Line Tax',0.03,NULL)
,(235736, 'AZ','Stamping Fee',0.002,NULL
是延续。
最后一点,这些扩展已被广泛使用,并且在很大程度上没有争议;当您使用(大多数)类型的系统扩展名让您更直接地表达自己的意思时,您不必担心。