扩展代数数据类型

4
注意:如果这个问题有些奇怪,那是因为我最近才接触到Haskell,并且仍在适应函数式思维方式。
考虑像 Maybe 这样的数据类型:
data MyOwnMaybe a = MyOwnNothing | MyOwnJust a

每个使用我的数据类型的人都会编写类似以下的函数:
maybeToList :: MyOwnMaybe a -> [a]
maybeToList MyOwnNothing  = []
maybeToList (MyOwnJust x) = [x]

现在,假设我在以后的某个时间想要扩展这个数据类型

data MyOwnMaybe a = MyOwnNothing | MyOwnJust a | SuperpositionOfNothingAndJust a

我该如何确保每个人的函数都在编译时出错?

当然,有可能我没有“领悟”代数数据类型,也许我根本不应该这样做,但是考虑到一个数据类型 Action

data Action = Reset | Send | Remove

看起来添加额外的Action(如Add)并不那么罕见(我也不想冒险让所有这些函数无法处理我的新Action)。


1
你的问题真的是“确保每个人的函数在编译时都会崩溃”,还是如何防止任何人的代码崩溃?(两者都有意义) - leftaroundabout
2
似乎是表达式问题的轻微变化。 - Random Dev
我认为应该两者都考虑。我真的想确保程序(无论是我编写的部分还是使用我的代码的部分)都能按照预期运行。我觉得我正在冒很大风险,因为很多这些函数显然没有考虑到我的新“Add”功能。如果我正确的话,“掉进去”的情况会导致运行时异常。这将是非常不可取的。 - Werner de Groot
如果你添加了cases,代码应该如何突然知道如何按照预期行事呢? - Random Dev
在这种情况下,我更倾向于编译时出现错误,以便我知道需要重新考虑这些函数。这与我在结尾处使用maybeToList _ = []的情况不同。 - Werner de Groot
3个回答

3
好消息是,您在破坏接口之前其实可以走很长一段路。您只需要仔细考虑从模块中导出什么。如果您导出高层次函数,而不是其内部机制,则有很大的机会使用新数据类型重写这些函数,一切都会顺利进行。特别是在导出数据构造函数时要非常小心。在这种情况下,您不仅导出创建数据的函数,还导出了模式匹配的可能性;这并不是一件让您感到束缚的事情。因此,在您的示例中,如果编写以下函数:
myOwnNothing :: MyOwnMaybe a
myOwnJust :: a -> MyOwnMaybe a

并且

fromMyOwnMaybe :: MyOwnMaybe a -> b -> (a -> b) -> b
fromMyOwnMaybe MyOwnNothing b _ = b
fromMyOwnMaybe (MyOwnJust a) _ f = f a

如果你能重新实现更新后的MyOwnMaybe数据类型,那么假设你应该是可以这样做的。因此,只需导出这些函数和数据类型本身,但不要导出构造函数,这是合理的。
唯一需要导出构造函数的情况是当你绝对确定你的数据类型永远不会改变时。例如,Bool总是只有两个(完全定义)值:TrueFalse,它不会被某些FileNotFound之类的东西扩展(尽管Edward Kmett可能会不同意)。同样,Maybe[]也是如此。
但这个想法更普遍:尽可能保持高层次。

你是否建议提供一个函数,该函数接受x个函数(其中一些可能只是默认值,例如您为MyOwnNothing所做的那样),以提供对这些x种不同类型的访问权限? - Werner de Groot
这真的取决于预期的用途。我定义了 fromMyOwnMaybe,假设 MyOwnMaybe 将几乎与常规的 Maybe 有相同的作用。通常您需要考虑预期的使用模式 - 您对它了解什么,什么肯定应该在那里,什么不应该在那里。没有预定义的一套规则。如果您什么都不知道,那么这种类似于 fold 的方法可能是最好的。 - MigMit

3

您似乎知道 GHC 可以通过 -W 标志或显式地使用 -fwarn-incomplete-patterns 来警告函数中的非全面模式匹配。

关于为什么这些警告不会自动成为编译时错误,有一个很好的讨论在 SO 上:

Haskell 中为什么非全面模式不是编译时错误?

此外,请考虑以下情况,其中您拥有具有大量构造函数的 ADT:

data Alphabet = A | B | C | ... | X | Y | Z

isVowel :: Alphabet -> Bool
isVowel A = True
isVowel E = True
isVowel I = True
isVowel O = True
isVowel U = True
isVowel _ = False

默认情况被用作一种便利,以避免编写其他21个情况。

现在,如果您向 Alphabet 添加一个额外的构造函数, isVowel 是否应该被标记为“不完整”?


我不知道这个!有没有一种方法可以“标记”应该完成(没有默认情况)的函数,并将其与无论是否添加新情况都能正常工作的函数区分开来?(无论是通过 -W 还是其他方式) - Werner de Groot
@WernerdeGroot 在 Haskell 中好像不行。我真的很希望能够要求编译器将非穷尽性视为错误。在其他一些语言中,例如 Coq 或 Agda,默认情况下就是这种情况。 - chi
你可以使用-Werror来完成,但这显然是一个包件交易。 - MasterMastic
1
@MasterMastic 我认为-Werror太过激进,因为它也会影响到其他所有警告。虽然这是一个选项,但我希望能够添加一些东西到我的.cabal文件中,而不必担心未来的编译器会变得太挑剔。 - chi
1
@chi,这很有道理。将“-ferror…”添加到“-fwarn…”和“-fno-warn…”中。 - dfeuer

1

许多模块做的一件事是不导出它们的构造函数。相反,它们导出可用的函数(“智能构造函数”)。如果稍后更改抽象数据类型,则必须在模块中修复函数,但不会破坏其他人的代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接