【问题标题】:How can I make the signature of this function more precise我怎样才能使这个函数的签名更精确
【发布时间】:2019-06-19 02:56:10
【问题描述】:

我有两个功能:

prompt :: Text -> (Text -> Either Text a) -> IO a
subPrompt :: Text -> (Text -> Bool) -> IO a -> IO (Maybe (Text, a))

subPrompt 接受第二个提示(参数 3),如果参数 2 中的函数在运行第一个提示后返回为真,则显示它。

我不喜欢参数 3 是 IO a 我希望它更像:

subPrompt :: Text -> (Text -> Bool) -> prompt -> IO (Maybe (Text, a))

但我知道我做不到。我一直在想办法让签名更清楚第三个参数是什么。有什么方法可以定义更清晰的类型吗?或者我想太多了,IO a 实际上很好 - 我对 haskell 还是很陌生。

【问题讨论】:

  • 您希望能够通过哪些非IOy 的示例? subPrompt 应该如何处理它们?
  • 哦,不,总是 IO。我想我是在问是否有办法让它成为一种特定类型的 IO。
  • 好吧,我想我明白了:你想确保subPrompt 的第三个参数实际上是对prompt 的调用(而不是,比如说deleteDirectory "/"),对吗?我已经写了一个关于一种方法的答案,但在写它时我思考:你是否也希望允许将subPrompt 的结果作为第三个参数传递给subPrompt? (在我的回答中,我假设“是”,但当然也可以适应“否”的情况。)
  • 你想的没错 :) 我希望能够有任意数量的 subPrompts

标签: haskell types


【解决方案1】:

一种方法是将这两个东西具体化为一个数据结构。所以:

{-# LANGUAGE GADTs #-}

data Prompt a where
    Prompt :: Text -> (Text -> Either Text a) -> Prompt a
    SubPrompt :: Text -> (Text -> Bool) -> Prompt a -> Prompt (Maybe (Text, a))

现在因为SubPrompt 的第三个参数是Prompt,所以您知道它必须是对SubPromptPrompt 的调用——绝对不是一些可能进行文件系统访问的任意IO 操作或其他一些讨厌的事情。

然后你可以为这个微型 DSL 写一个解释器到IO

runPrompt :: Prompt a -> IO a
runPrompt (Prompt cue validator) = {- what your old prompt used to do -}
runPrompt (SubPrompt cue deeper sub) = {- what your old subPrompt used to do,
                                          calling runPrompt on sub where needed -}

除了确保您没有任意IO 作为SubPrompt 的参数的好处之外,这还有一个好处是它使测试更容易。稍后您可以实现第二个完全纯的解释器;比如说,类似这样的东西,它将一个文本列表视为用户输入,并返回一个提示输出的文本列表:

data PromptResult a = Done a | NeedsMoreInput (Prompt a)

purePrompt :: Prompt a -> [Text] -> ([Text], PromptResult a)
purePrompt = {- ... -}

【讨论】:

  • 这是一个很棒的答案。不过,我的项目已经在使用以前的样式,这就是我接受 typedfern 答案的原因。但我会强烈考虑在未来重构为这种风格。谢谢!
【解决方案2】:

将第二个提示设置为简单的IO a 并没有错 - 特别是如果您在某处记录它是什么。

也就是说,是的,使类型尽可能不言自明是一种很好的做法;你可以创建一个别名:

type Prompt a = IO a

然后在 subPrompt 的签名中使用:

subPrompt :: Text -> (Text -> Bool) -> Prompt a -> IO (Maybe (Text, a))

这使得签名更加不言自明,同时仍然允许您将任何IO a 作为第三个参数传递(关键字type 只是创建一个别名)。

但是等等,还有更多:你宁愿不要意外传递任何实际上不是提示的IO a!你不想给它传递一个 IO 动作,比如发射导弹......

所以,我们可以声明一个实际的 Prompt 类型(不仅仅是一个别名,而是一个真正的类型):

newtype Prompt a = Prompt { getPrompt :: IO a }

这允许您将IO a 类型的任何值包装在一个类型中,确保它不会与具有相同类型但语义不同的其他函数混淆。

subPrompt 的签名和之前一样:

subPrompt :: Text -> (Text -> Bool) -> Prompt a -> IO (Maybe (Text, a))

但现在你不能将任何旧的IO a 传递给它;例如,要传递您的prompt,您必须将其包装起来:

subPrompt "Do we proceed?" askYesNo (Prompt (prompt "Please enter your name" processName))

(subPrompt 将无法直接调用它,但必须从包装器内部提取“提示”:let actualPrompt = getPrompt wrappedPrompt

【讨论】:

  • 啊,newtype 正是我所需要的。我之前一直在为 type 和 newtype 之间的区别而苦恼。但这让事情变得一清二楚——正如你所说,它阻止了意外情况。谢谢!
  • 如果将prompt(和subPrompt?)的类型更改为返回Prompts 而不是IOs 并讨论如何使用模块边界来确保用户不会意外地将任意的IO 操作变成Prompts。
猜你喜欢
  • 2012-08-02
  • 2016-03-07
  • 2021-05-23
  • 2011-04-27
  • 2014-01-07
  • 2018-03-25
  • 1970-01-01
  • 1970-01-01
  • 2016-06-15
相关资源
最近更新 更多